Browse Source

added zustand blog post

theming
Will Webberley 2 months ago
parent
commit
71c1fea11d
9 changed files with 155 additions and 184 deletions
  1. 2
      gatsby-browser.js
  2. 16
      gatsby-config.js
  3. 2
      package.json
  4. 68
      src/components/Layout/Layout.css
  5. 1
      src/components/Layout/Layout.js
  6. 143
      src/components/Layout/prism.css
  7. 46
      src/pages/blog/2021-02-05-react-state-zustand.md
  8. 9
      src/templates/blog-post.js
  9. 52
      yarn.lock

2
gatsby-browser.js

@ -0,0 +1,2 @@
require("prismjs/themes/prism-solarizedlight.css");
require("prismjs/plugins/line-numbers/prism-line-numbers.css");

16
gatsby-config.js

@ -114,7 +114,21 @@ module.exports = {
],
},
},
'gatsby-transformer-remark',
{
resolve: `gatsby-transformer-remark`,
options: {
plugins: [
{
resolve: `gatsby-remark-prismjs`,
options: {
classPrefix: "language-",
showLineNumbers: true,
noInlineHighlight: false,
}
}
]
},
},
'gatsby-plugin-react-helmet',
],
};

2
package.json

@ -9,8 +9,10 @@
"gatsby": "^2.31.0",
"gatsby-plugin-feed": "^2.12.0",
"gatsby-plugin-react-helmet": "^3.1.3",
"gatsby-remark-prismjs": "^3.13.0",
"gatsby-source-filesystem": "^2.10.0",
"gatsby-transformer-remark": "^2.6.14",
"prismjs": "^1.23.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-helmet": "^5.2.1",

68
src/components/Layout/Layout.css

@ -138,22 +138,56 @@ article strong{
font-variation-settings: var(--mono), var(--casl), var(--wght), var(--slnt), var(--CRSV);
}
code{
font-family: 'Recursive', monospace;
--mono: "MONO" 1; /* 0-1 */
--casl: "CASL" 0.5; /* 0-1 */
--wght: "wght" 400;
--slnt: "slnt" -5; /* 0 - -15 */
--CRSV: "CRSV" 0;
font-variation-settings: var(--mono), var(--casl), var(--wght), var(--slnt), var(--CRSV);
background:rgb(240,240,240);
color:#8C3667;
padding:2px 3px;
font-size:14px;
border-radius:2px;
/**
* If you already use line highlighting
*/
/* Adjust the position of the line numbers */
.gatsby-highlight pre[class*="language-"].line-numbers {
padding-left: 2.8em;
}
pre code{
display:block;
white-space: pre-wrap !important;
padding: 10px;
/**
* If you only want to use line numbering
*/
.gatsby-highlight {
border-radius: 0.3em;
margin: 0.5em 0;
padding: 1em;
overflow: auto;
}
.gatsby-highlight pre[class*="language-"].line-numbers {
padding: 0;
padding-left: 2.8em;
overflow: initial;
}
/**
* Add back the container background-color, border-radius, padding, margin
* and overflow that we removed from <pre>.
*/
.gatsby-highlight {
background-color: #fdf6e3;
border-radius: 0.3em;
margin: 0.5em 0;
padding: 1em;
overflow: auto;
}
/**
* Remove the default PrismJS theme background-color, border-radius, margin,
* padding and overflow.
* 1. Make the element just wide enough to fit its content.
* 2. Always fill the visible space in .gatsby-highlight.
* 3. Adjust the position of the line numbers
*/
.gatsby-highlight pre[class*="language-"] {
background-color: transparent;
margin: 0;
padding: 0;
overflow: initial;
float: left; /* 1 */
min-width: 100%; /* 2 */
}

1
src/components/Layout/Layout.js

@ -4,7 +4,6 @@ import Helmet from 'react-helmet'
import Header from '../Header'
import Footer from '../Footer';
import './Layout.css'
import './prism.css'
const TemplateWrapper = ({ children }) => (
<div>

143
src/components/Layout/prism.css

@ -1,143 +0,0 @@
/* PrismJS 1.23.0
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+bash+python */
/**
* prism.js default theme for JavaScript, CSS and HTML
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
code[class*="language-"],
pre[class*="language-"] {
color: black;
background: none;
text-shadow: 0 1px white;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
font-size: 1em;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
text-shadow: none;
background: #b3d4fc;
}
pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
code[class*="language-"]::selection, code[class*="language-"] ::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none;
}
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #f5f2f0;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #999;
}
.token.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #905;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #690;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #9a6e3a;
/* This background color was intended by the author of this theme. */
background: hsla(0, 0%, 100%, .5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.function,
.token.class-name {
color: #DD4A68;
}
.token.regex,
.token.important,
.token.variable {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

src/pages/blog/2021-02-04-zustand.md → src/pages/blog/2021-02-05-react-state-zustand.md

@ -1,14 +1,17 @@
---
date: "2021-02-04T22:16:00Z"
date: "2021-02-05T23:46:00Z"
title: "React State Management with Zustand"
description: "How to manage your React app's global state using the zustand library."
description: "How to manage your JavaScript React app's global state using the zustand library."
tags: [100daystooffload, technology, javascript, react]
---
## React state
React state management is what gives the library its reactiveness. It's what makes it so easy to build performant data-driven applications that dynamically update based on the underlying data. In this example the app would automatically update the calculation result as the user types in the input boxes:
```
```jsx
import React, { useState } from 'react';
function MultiplicationCalculator() {
const [number1, setNumber1] = useState(0);
const [number2, setNumber2] = useState(0);
@ -22,36 +25,41 @@ function MultiplicationCalculator() {
![The resultant React app, showing two text inputs and a result line](/media/blog/zustand1.png)
The entire function will re-run on each state change (the `setNumber1` and `setNumber2` functions) in order to reactively update what is displayed. The multiplication itself could be calcualted in a `useEffect` but is simpler to look at it this way.
The entire function will re-run on each state change (the `setNumber1` and `setNumber2` functions) in order to reactively update the result text. The multiplication itself could be calculated in a `useEffect` but it is simpler to look at it as shown.
This is totally fine for most apps, however this quickly becomes unmanageable when you need to share state (e.g. `number1`) between this component and another component - and ensure that a state change in the former can be reflected in the latter - whether it's an ancestor, descendant, or a more distant component. Of course, you can pass the state variables (and the associated `setState` functions) from a parent down as `props` to child components, but as soon as you're doing this more than a handful of times or in cases where state needs to be shared across distant components this quickly becomes hard to maintain or understand.
This is totally fine for many apps, however this quickly becomes unmanageable when you need to share state (e.g. `number1`) between this component and another component - and ensure that a state change in the former can be reflected in the latter - whether it's an ancestor, descendant, or a more distant component. Of course, you can pass the state variables (and the associated `setState` functions) from a parent down as `props` to child components, but as soon as you're doing this more than a handful of times or in cases where state needs to be shared across distant components this quickly becomes hard to maintain or understand.
An example of shared state might be to store the details about the currently logged-in user in an app. A navigation bar component would need to know about the user state to show a link to the correct profile page, and another component may need access to the same state in order to allow the user to change their name.
This is by no means a new problem. Many of these issues are solved using React's [Context](https://reactjs.org/docs/context.html) and there are also libraries like Redux that are useful in perhaps more complex scenarios - it's much more opinionated and involves a fair bit of extra code that may be overkill in many apps. Adding just a small piece of state (e.g. a new text input), and the ability to alter it, to Redux involves updating reducers, creating an action, dispatchers, and wiring things through to your containers using `connect`, `mapStateToProps`, and `mapDispatchToProps`. Plus you'll need the relevant provider higher up.
## Context and Redux
Redux is certainly a fantastic library, however, and I use it in many apps. [This post](https://changelog.com/posts/when-and-when-not-to-reach-for-redux) is useful and talks about the cases in which you may (or may not) want to use Redux.
This is by no means a new problem. Many of these issues are solved using React's [Context API](https://reactjs.org/docs/context.html) and there are also libraries like Redux that are useful in perhaps more complex scenarios - it's much more opinionated and involves a fair bit of extra code that may be overkill in many apps. Adding just a small piece of state (e.g. a new text input), and the ability to alter it, to Redux involves updating reducers, creating an action, dispatchers, and wiring things through to your components using `connect`, `mapStateToProps`, and `mapDispatchToProps`. Plus you'll need the relevant provider higher up.
In this post I want to talk about another option that is perhaps quicker and easier to use, expecially for those newer to React (though is also great for more seasoned React developers) - [zustand](https://github.com/pmndrs/zustand). Not only is this the German word for "state", it's also a nice and succint library for state management for React.
Redux is certainly a fantastic library, however, and I use it in many apps. [This post](https://changelog.com/posts/when-and-when-not-to-reach-for-redux) is useful and discusses the cases in which you may (or may not) want to use Redux.
The zustand library is pretty concise, so you shouldn't need to add too much extra code. To get started just add it as a dependency to your project (e.g. `yarn add zustand`). Now let's rewrite the above multiplication example but using zustand.
## Zustand
First, define a _store_ for your app. This will contain a reference to all of the values you want to keep in your global state, as well as the functions that allow those values to change (_mutators_). In our store, we'll extract out the state for `number1` and `number2` in our component from earlier, and the appropriate update functions (e.g. `setNumber1`) into the store:
In this post I want to talk about another option that is perhaps quicker and easier to use, expecially for those newer to React (though it's also great for more seasoned React developers) - [zustand](https://github.com/pmndrs/zustand). Not only is this the German word for "state", it's also a nice and succinct library for state management for React.
```
The zustand library is pretty concise, so you shouldn't need to add too much extra code. To get started just add it as a dependency to your project (e.g. `yarn add zustand`). Now let's rewrite the earlier multiplication example but using zustand.
First, define a _store_ for your app. This will contain all of the values you want to keep in your global state, as well as the functions that allow those values to change (_mutators_). In our store, we'll extract out the state for `number1` and `number2` we used in our component from earlier, and the appropriate update functions (e.g. `setNumber1`), into the store:
```jsx
import React from 'react';
import create from 'zustand';
const useStore = create((set) => ({
number1: 0,
number2: 0,
setNumber1: (x) => set((state) => ({ number1: x })),
setNumber2: (x) => set((state) => ({ number2: x })),
setNumber1: (x) => set(() => ({ number1: x })),
setNumber2: (x) => set(() => ({ number2: x })),
}));
```
Next we can go ahead and rewrite our component such that it now uses this store instead of its own local state:
Now - in the same file - we can go ahead and rewrite our component such that it now uses this store instead of its own local state:
```
import React from 'react';
```jsx{numberLines: 10}
function MultiplicationCalculator() {
const { number1, number2, setNumber1, setNumber2 } = useStore();
return ( <>
@ -64,10 +72,10 @@ function MultiplicationCalculator() {
That's it - we now have a React app that uses zustand. As before, the component function runs each time the store's state changes, and zustand ensures things are kept up-to-date.
In this example the two blocks of code above could be in the same file. However, the power of zustand becomes particularly useful when the store is shared amongst several components across different parts of your app.
In the example above the two blocks of code are in the same file. However, the power of zustand becomes particularly useful when the store is shared amongst several components across different parts of your app to provide "global state".
For example, the `useStore` variable could be declared and exported from a file named `store.js` somewhere in your app's file structure. Then, when a component needs to access its variables or mutator functions it just needs to - for example, `import useStore from path/to/store` - and then use [object destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) (as above) to pull out the needed variables and functions.
For example, the `useStore` variable could be declared and exported from a file named `store.js` somewhere in your app's file structure. Then, when a component needs to access its variables or mutator functions it just needs to - for example, `import useStore from 'path/to/store'` - and then use [object destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) (as on line 11 above) to pull out the needed variables and functions.
It's worth checking out [the documentation](https://github.com/pmndrs/zustand) since zustand can be super flexible and can be used in ways to help improve performance, such as memoizing and state slicing. It also makes what can be tricky in other such libraries - e.g. asynchronous state updates - trivial.
It's worth checking out [the documentation](https://github.com/pmndrs/zustand) since zustand is super flexible and can be used in ways that help improve performance, such as taking advantage of memoizing and state slicing. It also makes what can be tricky in other such libraries - e.g. asynchronous state updates - trivial.
If you've already got an established app using another state management system it may not be worth migrating everything over. But give zustand a go in your next project if you're looking for straight forward, yet powerful, state management.

9
src/templates/blog-post.js

@ -58,9 +58,12 @@ export default class BlogPost extends React.Component {
<div dangerouslySetInnerHTML={{ __html: post.html }} />
</article>
<p>--</p>
<p><em>This article is part of a collection of posts involved in the <ExternalLink href="https://100daystooffload.com">#100DaysToOffload</ExternalLink> series. As such it may have been written quickly and should be considered more as a thought "dump" rather than a fully-fledged essay. Thanks for reading!</em></p>
{post.frontmatter.tags && post.frontmatter.tags.indexOf('100daystooffload') > -1 &&
<>
<p>--</p>
<p><em>This article is part of a collection of posts involved in the <ExternalLink href="https://100daystooffload.com">#100DaysToOffload</ExternalLink> series. As such it may have been written quickly and should be considered more as a thought "dump" rather than a fully-fledged essay. Thanks for reading!</em></p>
</>
}
{post.frontmatter.tags && post.frontmatter.tags.indexOf('book') > -1 &&
<div style={{background: 'linen', padding: 4, borderRadius: 3, margin: '20px 0px'}}>

52
yarn.lock

@ -2973,6 +2973,15 @@ cli-width@^3.0.0:
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==
clipboard@^2.0.0:
version "2.0.6"
resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.6.tgz#52921296eec0fdf77ead1749421b21c968647376"
integrity sha512-g5zbiixBRk/wyKakSwCKd7vQXDjFnAMGHoEyBogG/bw9kTD9GvdAvaoRR1ALcEzt3pVKxZR0pViekPMIS0QyGg==
dependencies:
good-listener "^1.2.2"
select "^1.1.2"
tiny-emitter "^2.0.0"
clipboardy@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-2.3.0.tgz#3c2903650c68e46a91b388985bc2774287dba290"
@ -3813,6 +3822,11 @@ delayed-stream@~1.0.0:
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
delegate@^3.1.2:
version "3.2.0"
resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166"
integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==
depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
@ -5316,6 +5330,15 @@ gatsby-recipes@^0.8.0:
xstate "^4.9.1"
yoga-layout-prebuilt "^1.9.6"
gatsby-remark-prismjs@^3.13.0:
version "3.13.0"
resolved "https://registry.yarnpkg.com/gatsby-remark-prismjs/-/gatsby-remark-prismjs-3.13.0.tgz#010c4f9371154536018d4488ee474066a438a0cb"
integrity sha512-6dEuXqSCoxgfiArhiK+QK07IBuheoEyXrSd6o8mZ9zqId4Clp/p3ponANwoB2WYQSQmMG6eUOX+eV9qias3ECA==
dependencies:
"@babel/runtime" "^7.12.5"
parse-numeric-range "^0.0.2"
unist-util-visit "^1.4.1"
gatsby-source-filesystem@^2.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/gatsby-source-filesystem/-/gatsby-source-filesystem-2.10.0.tgz#9ee2f770d3ae15062a111bb1faea76b7d6e2b059"
@ -5732,6 +5755,13 @@ globby@^6.1.0:
pify "^2.0.0"
pinkie-promise "^2.0.0"
good-listener@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
integrity sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=
dependencies:
delegate "^3.1.2"
got@8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/got/-/got-8.3.2.tgz#1d23f64390e97f776cac52e5b936e5f514d2e937"
@ -8578,6 +8608,11 @@ parse-latin@^4.0.0:
unist-util-modify-children "^2.0.0"
unist-util-visit-children "^1.0.0"
parse-numeric-range@^0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/parse-numeric-range/-/parse-numeric-range-0.0.2.tgz#b4f09d413c7adbcd987f6e9233c7b4b210c938e4"
integrity sha1-tPCdQTx6282Yf26SM8e0shDJOOQ=
parse-passwd@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
@ -9232,6 +9267,13 @@ pretty-format@^25.5.0:
ansi-styles "^4.0.0"
react-is "^16.12.0"
prismjs@^1.23.0:
version "1.23.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33"
integrity sha512-c29LVsqOaLbBHuIbsTxaKENh1N2EQBOHaWv7gkHN4dgRbxSREqDnDbtFJYdpPauS4YCplMSNCABQ6Eeor69bAA==
optionalDependencies:
clipboard "^2.0.0"
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@ -10176,6 +10218,11 @@ select-hose@^2.0.0:
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=
select@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=
selfsigned@^1.10.8:
version "1.10.8"
resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.8.tgz#0d17208b7d12c33f8eac85c41835f27fc3d81a30"
@ -11173,6 +11220,11 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
tiny-emitter@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"

Loading…
Cancel
Save