Skip to content

Commit

Permalink
Migrated blog to MDX (#13)
Browse files Browse the repository at this point in the history
* Migrated blog to MDX

* Added missing dependencies

* Added .nvmrc for latest LTS
  • Loading branch information
rtbenfield authored May 31, 2021
1 parent ff8252b commit b831d66
Show file tree
Hide file tree
Showing 16 changed files with 1,625 additions and 486 deletions.
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lts/*
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,6 @@ The CodeSandbox example below has 3 components:
Each component and function has `console.log` added to track rerenders.
Play around with adding and updating items and observe the Console tab below the preview.

<iframe src="https://codesandbox.io/embed/bitter-wave-1yv1g?expanddevtools=1&fontsize=14&module=%2Fsrc%2FTodoList.jsx" title="react-context-performance-example1" allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>

There are a few things to notice from the logs in this baseline example.

First, any input to the field in `AddTodo` rerenders only that component.
Expand All @@ -77,8 +75,6 @@ Notice how fewer props we have being passed around now. Each component instead s
> Using Context for something this simple is absolutely overkill, but this is just an example.
> I'm not suggesting you go switch all of your state to Context.
<iframe src="https://codesandbox.io/embed/react-context-performance-example1-hf9uq?expanddevtools=1&fontsize=14&module=%2Fsrc%2FtodoContext.jsx" title="react-context-performance-example2" allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>

Notice that this didn't eliminate any component renders. In fact, it added the `TodoContextProvider` render.
That's because all of our components are subscribing to the context value with `useTodos`.
Even if they didn't, the "root" `TodoList` component subscribes to it, so the rerender that happens there will cascade to the other components anyway.
Expand Down Expand Up @@ -115,17 +111,13 @@ By doing this we remove the `useTodos` dependency from `AddTodo` itself, and `Re
To be fair, we haven't reduced the _number_ of rerenders, but we did exchange an expensive rerender for an inexpensive one.
All of this can be done within the internal scope of `AddTodo.jsx` and none of the other components in the tree know the difference.

<iframe src="https://codesandbox.io/embed/react-context-performance-example2-2u1lp?expanddevtools=1&fontsize=14&module=%2Fsrc%2FtodoContext.jsx" title="react-context-performance-example3" allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>

Let's keep going! What if we don't want to rerender all `TodoListItem` (again, **only if it is expensive**) components for a change to a single item, like changing `isComplete`?
We can take a similar approach to subscribe to `TodoListItem` to a single todo item.
By wrapping `TodoListItem` in another component that extracts the values we need from `useTodos` and passes them as props, we can rely on `React.memo` to skip rerendering.
As long as the `todo` prop's reference is maintained, we skip rerendering `TodoListItem`!
This **only** works because of our use of immutability in `updateTodo`.
Otherwise, we would have just introduced a bug because `TodoListItem` would not have rerendered changes made to the item!

<iframe src="https://codesandbox.io/embed/react-context-performance-example3-2fuh0?expanddevtools=1&fontsize=14&module=%2Fsrc%2FTodoListItem.jsx" title="react-context-performance-example4" allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>

## Redux didn't need this!

Actually, it did. Redux requires you to connect to your global store using selectors and `connect`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ I usually like to start by thinking about what the API surface (props) of my com
Maybe we have a component that displays charts like this:

```js
const MyApp = () => {
function MyApp() {
return (
<Chart
data={data}
Expand All @@ -54,15 +54,15 @@ const MyApp = () => {
]}
/>
);
};
}
```

This is a pretty small example, but you can imagine how much more complex it gets as the number of options increases.
What would this API look like if were more component-based?
We might have something more like this:

```js
const MyApp = () => {
function MyApp() {
return (
<Chart data={data}>
<LineChart
Expand All @@ -83,15 +83,15 @@ const MyApp = () => {
/>
</Chart>
);
};
}
```

Doesn't that read better? Well, maybe that's a matter of preference, but it does open the door for some pretty powerful reusability.
If all of the configuration "parts" are components, nothing is stopping us from using other tools React gives us with components.
We might define a reusable `LineChart` configuration like this:

```js
const MyLineChart = (props) => {
function MyLineChart(props) {
return (
<LineChart
color="blue"
Expand All @@ -102,22 +102,22 @@ const MyLineChart = (props) => {
{...props}
/>
);
};
}

const MyApp = () => {
function MyApp() {
return (
<Chart data={data}>
<MyLineChart dataKey="value1" />
<MyLineChart color="red" dataKey="value2" />
</Chart>
);
};
}
```

We could even tie it into other things, like Context, to use themes or translations:

```js
const MyLineChart = (props) => {
function MyLineChart(props) {
const { formatValue } = useLocalizationContext();
const { getChartColor } = useTheme();
return (
Expand All @@ -130,16 +130,16 @@ const MyLineChart = (props) => {
{...props}
/>
);
};
}

const MyApp = () => {
function MyApp() {
return (
<Chart data={data}>
<MyLineChart dataKey="value1" />
<MyLineChart dataKey="value2" />
</Chart>
);
};
}
```

This is only scratching the surface!
Expand All @@ -157,7 +157,7 @@ What state do we have? Take a look:
```js
const chartContext = React.createContext();

export const ChartContextProvider = ({ children }) => {
export function ChartContextProvider({ children }) {
const [charts, setCharts] = React.useState([]);

const registerChart = React.useCallback((chart) => {
Expand All @@ -183,7 +183,7 @@ export const ChartContextProvider = ({ children }) => {
return (
<chartContext.Provider value={value}>{children}</chartContext.Provider>
);
};
}

// Convenience hook so that chartContext is not exposed outside this module
export function useChartContext() {
Expand All @@ -199,17 +199,17 @@ export function useChartContext() {
What good does this give us? Let's start using it in `Chart` and `LineChart`.

```js
export const Chart = ({ children, data }) => {
export function Chart({ children, data }) {
return (
<ChartContextProvider>
{/* children will contain the LineChart instances */}
{children}
<ChartDisplay data={data} />
</ChartContextProvider>
);
};
}

export const LineChart = ({ color, dataKey, plotPoints, tooltip }) => {
export function LineChart({ color, dataKey, plotPoints, tooltip }) {
const { registerChart, unregisterChart } = useChartContext();

// Any time one of the props changes, re-register the chart object definition
Expand All @@ -225,13 +225,13 @@ export const LineChart = ({ color, dataKey, plotPoints, tooltip }) => {

// LineChart doesn't actually render anything!
return null;
};
}

const ChartDisplay = ({ data }) => {
function ChartDisplay({ data }) {
const { charts } = useChartContext();

// TODO: Draw the actual chart using the `charts` object and `data`
};
}
```

That's all there is to it!
Expand All @@ -253,7 +253,7 @@ Say we want to build a component that displays a tab strip, manages the selected
Just like above, let's start with what we want the API surface to look like:

```js
const MyApp = () => {
function MyApp() {
const [selectedTab, setSelectedTab] = React.useState(1);
return (
<Tabs onChange={setSelectedTab} selectedTab={selectedTab}>
Expand All @@ -268,7 +268,7 @@ const MyApp = () => {
</Tab>
</Tabs>
);
};
}
```

That seems simple, but how does the `Tabs` component know when our `Tab` is clicked?
Expand All @@ -279,7 +279,7 @@ This feels like the right API, so let's use Context to make it happen!
```js
const tabContext = React.createContext();

const TabContextProvider = ({ children, onChange, selectedTab }) => {
function TabContextProvider({ children, onChange, selectedTab }) {
const [tabs, setTabs] = React.useState([]);

const registerTab = React.useCallback((tab) => {
Expand All @@ -306,7 +306,8 @@ const TabContextProvider = ({ children, onChange, selectedTab }) => {
{children}
</tabContext.Provider>
)
};
}

// Convenience hook so that chartContext is not exposed outside this module
export function useTabContext() {
const value = React.useContext(tabContext);
Expand All @@ -317,16 +318,16 @@ export function useTabContext() {
return value;
}

const Tabs = ({ children, onChange, selectedTab }) => {
function Tabs({ children, onChange, selectedTab }) {
return (
<TabContextProvider onChange={onChange} selectedTab={selectedTab}>
<TabDisplay />
{children}
<TabContextProvider>
);
};
}

const Tab = ({ children, label, tabKey }) => {
function Tab({ children, label, tabKey }) {
const { registerTab, unregisterTab, selectedTab } = useTabContext();

React.useEffect(() => {
Expand All @@ -343,13 +344,13 @@ const Tab = ({ children, label, tabKey }) => {
} else {
return null;
}
};
}

const TabDisplay = () => {
function TabDisplay() {
const { tabs, selectedTab, setSelectedTab } = useTabContext();

// TODO: Display tab strip using `tabs`
};
}
```

Notice how similar this was to the chart example.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ class ErrorBoundary extends React.Component {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
} else {
return this.props.children;
}

return this.props.children;
}
}
```
Expand All @@ -55,24 +55,27 @@ The React docs on error boundaries show an example of how we can handle event ha
We can put the error in state and then use it in `render` to show a fallback display.
No magic, no error boundary, just using simple React primitives.

```jsx
const MyComponent = () => {
```jsx live=true
function MyComponent() {
const [error, setError] = React.useState(null);

function handleClick() {
try {
// Do something that could throw
throw Error("oops!");
} catch (err) {
setError(err);
}
}

if (error) {
return <h1>Caught an error.</h1>;
return <button onClick={() => setError(null)}>Error! Reset</button>;
} else {
return <div onClick={handleClick}>Click Me</div>;
return <button onClick={handleClick}>Click Me</button>;
}
};
}

render(<MyComponent />);
```

That's great, but what if we don't want all of our components to deal with errors themselves?
Expand All @@ -84,8 +87,33 @@ So what do we do?

Just re-throw the error in `render`. **What?!?** Yup. Check it out.

```jsx
const MyComponent = () => {
```jsx live=true
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}

render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return (
<button onClick={() => this.setState({ hasError: false })}>
Error! Reset
</button>
);
} else {
return this.props.children;
}
}
}

function MyComponent() {
const [error, setError] = React.useState(null);
if (error) {
throw error;
Expand All @@ -94,13 +122,20 @@ const MyComponent = () => {
function handleClick() {
try {
// Do something that could throw
throw new Error("oops!");
} catch (err) {
setError(err);
}
}

return <div onClick={handleClick}>Click Me</div>;
};
return <button onClick={handleClick}>Click Me</button>;
}

render(
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>,
);
```

That's all it takes. No magic, no special syntax, no utility functions, just plain JavaScript.
Expand Down
File renamed without changes.
File renamed without changes.
15 changes: 15 additions & 0 deletions jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"checkJs": true,
"module": "es2020",
"jsx": "react",
"moduleResolution": "node",
"target": "es2020"
},
"exclude": [
"node_modules"
],
"include": [
"src/**/*"
]
}
9 changes: 7 additions & 2 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
module.exports = {
const withMDX = require("@next/mdx")({
extension: /\.mdx$/,
});

module.exports = withMDX({
future: {
webpack5: true,
},
};
pageExtensions: ["js", "jsx", "md", "mdx"],
});
Loading

0 comments on commit b831d66

Please sign in to comment.