diff --git a/.github/workflows/example-previews.yml b/.github/workflows/example-previews.yml index e23693439a87..ec23030b5ac4 100644 --- a/.github/workflows/example-previews.yml +++ b/.github/workflows/example-previews.yml @@ -64,7 +64,7 @@ jobs: steps: - name: Set Example Name id: set-example-name - run: echo 'EXAMPLE_NAME=$(echo ${ github.event.comment.body } | cut -f 2 -d " ")' >> $GITHUB_ENV + run: echo "EXAMPLE_NAME=$(echo ${{ github.event.comment.body }} | cut -f 2 -d ' ')" >> $GITHUB_ENV - uses: actions/checkout@v4 - uses: pnpm/action-setup@v3 with: @@ -78,18 +78,18 @@ jobs: - name: Build example run: pnpm build --scope ${{ env.EXAMPLE_NAME }} - name: test path - run: ls -l ${{ format('./examples/{0}/{1}', steps.set-example-name.outputs.example_name, contains(fromJson('["finefoods-client"]'), steps.set-example-name.outputs.example_name) && '.next' || 'dist') }} + run: ls -l ${{ format('./examples/{0}/{1}', env.EXAMPLE_NAME, contains(fromJson('["finefoods-client"]'), env.EXAMPLE_NAME) && '.next' || 'dist') }} - name: Deploy to Netlify uses: nwtgck/actions-netlify@v1.2 with: - publish-dir: ${{ format('./examples/{0}/{1}', steps.set-example-name.outputs.example_name, contains(fromJson('["finefoods-client"]'), steps.set-example-name.outputs.example_name) && '.next' || 'dist') }} + publish-dir: ${{ format('./examples/{0}/{1}', env.EXAMPLE_NAME, contains(fromJson('["finefoods-client"]'), env.EXAMPLE_NAME) && '.next' || 'dist') }} github-token: ${{ secrets.PANKOD_BOT_TOKEN }} deploy-message: "Deploy from GitHub Actions" - alias: deploy-preview-${{ steps.set-example-name.outputs.example_name }}-${{ github.event.number }} + alias: deploy-preview-${{ env.EXAMPLE_NAME }}-${{ github.event.comment.node_id }} enable-pull-request-comment: false overwrites-pull-request-comment: false - github-deployment-environment: "deploy-preview-${{ steps.set-example-name.outputs.example_name }}-${{ github.event.number }}" - netlify-config-path: ./examples/${{ steps.set-example-name.outputs.example_name }}/netlify.toml + github-deployment-environment: "deploy-preview-${{ env.EXAMPLE_NAME }}-${{ github.event.comment.node_id }}" + netlify-config-path: ./examples/${{ env.EXAMPLE_NAME }}/netlify.toml env: NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} diff --git a/documentation/blog/2024-05-28-react-usereducer.md b/documentation/blog/2024-05-28-react-usereducer.md new file mode 100644 index 000000000000..580575b4bd91 --- /dev/null +++ b/documentation/blog/2024-05-28-react-usereducer.md @@ -0,0 +1,889 @@ +--- +title: React useReducer Hook - The Basics +description: This post is about the useReducer() hook in React. We demonstrate with examples how to use it for action based state updates and discuss some safe practices. +slug: react-usereducer +authors: abdullah_numan +tags: [react] +image: https://refine.ams3.cdn.digitaloceanspaces.com/blog/2024-05-28-react-usereducer/social.png +hide_table_of_contents: false +--- + +## Introduction + +React [`useReducer()`](https://react.dev/reference/react/useReducer) hook is a state hook used often as a versatile alternative to `useState()`. It helps aggregate multiple states of a component in one place, particularly in scenarios that involve the state's changes at multiple nesting levels, and originating from multiple action types and sources. `useReducer()` gives access to data and actions defined in a React reducer. It exposes the `state` for a component to consume, and a `dispatch()` function to invoke actions that alters the `state`. + +In this post, we cover the basics of implementing consolidated state management with a reducer and the `useReducer()` hook, along with some essential good practices associated with writing efficient reducer functions. + +We first spend time to understand what essentially makes a React reducer, how it works and when to use one. That it consists of a aggregated state, a set of action `type`s and a reducer function that defines and implements those action types. Then, with examples from a small demo **Posts** app, we explore how `useReducer()` is used to access a reducer `posts` state and its `dispatch()` function. + +We demonstrate how to consume reducer data (stored as `posts`) and present them in JSX. While doing so, we also cover how to invoke actions with `dispatch()` by passing desired types to it (`like` and `unlike`). We discuss the importance of a reducer function and acknowledge the painstaking care necessary while writing one. We analyze relevant code snippets to show some good practices involved in efficiently composing React reducer functions that is used by a `useReducer()` hook. We spare time to understand the significance of initial reducer state and examine examples of how to pass one to `useReducer()`. + +In the later half, we expand our example to demonstrate how `useReducer()` enables state updates on multiple nesting levels. We add `create` and `delete` action types to the topmost level of `posts` that allow creating and deleting a post respectively. We elucidate with an example how to use a state initializer function for enhancing performance of a reducer. Towards the end, disucss the benefits of using `useReducer()`, how reducers are vital in Redux and how `useReducer()` and Redux compare. + +Steps we follow in this post are as follows: + +- [What is React useReducer ?](#what-is-react-usereducer) +- [React State Manipulation with useReducer: A Demo Posts App](#react-state-manipulation-with-usereducer-a-demo-posts-app) +- [React useReducer: Action on Multiple Levels](#react-usereducer-action-on-multiple-levels) +- [React useReducer: Benefits and Comparison](#react-usereducer-benefits-and-comparison) + +## What is React useReducer ? + +`useReducer()` is a React state hook which promotes an "action" based state management paradigm that involves component state consolidated or **_reduced_** in a reducer object. The state is manipulated by dispatching different types of actions, contrary to using a single setter function typical of `useState()`. Using actions to change state of reducer makes it a versatile tool -- as doing so allows a myriad of actions to defined and performed on the aggregated state. + +### React Reducers with useReducer + +Reducers get their name because they accummulate data into one piece in a way similar to how the JavaScript `Array.prototype.reduce()` method does. Reducers make the heart of dedicated global state management solutions like [Redux](https://redux.js.org/introduction/getting-started) and [RTK Query](https://redux-toolkit.js.org/tutorials/rtk-query) (Redux Toolkit Query). + +Reducers in plain React, though, are used for smaller scale local state management in components, as opposed to managing global state involving multiple entities and features of the app. + +For setting up and handling plain React reducers, we use the `useReducer()` hook. It is important to note that in many scenarios, `useState()` can be used to manage trivial, one-off local states. Only when complexity of local state becomes unmanageable with `useState()`, yet tied together around one application entity or closely related actions, we think of using a reducer in conjunction with the `useReducer()` hook. + +:::tip[Tip: When to Use React `useReducer()`] + +Typical use cases of React `useReducer()` hook arise when we need more types of change in a component state -- such as types of `like`s in a post: `like`, `unlike`, `angry`, `happy`, `love`, `celebrate`, etc. + +In general, we can use `useReducer()` in the following scenarios: + +- When we initialize multiple states using `useState()`, but they seem to be related and can be grouped or aggregated into one. +- We have multiple types of actions for altering the state. +- We have multiple types of events that trigger those actions. +- We have multiple sources of those actions, such as from different components and different levels of the component hierarchy. +- We have actions on multiple nesting levels of the state object. +- We need to reuse and reinitialize the reducer in different components. +- We can reuse the reducer for similar application entities. + +::: + +### How React useReducer Works + +The `useReducer()` hook in React accepts a reducer function and an initial state. It builds a reducer from these arguments and exposes the `state` and a `dispatch()` function for the component to use. For example, like this: + +```js +// Inside a component + +const [state, dispatch] = useReducer(postsReducer, initialPosts); + +// Consume `state` in JSX +// Invoke `dispatch()` with action type passed in button clicks +``` + +The `dispatch()` function is used to dispatch action types invoked from an event handler, such as on an `onClick` event. `dispatch()` typically takes a `type` prop in an argument object passed to it. More details later in [this section](#react-usereducer-how-to-initialize-an-action-based-reducer). + +### Reducer Functions in React + +The specifics of a dispatched action type, its implementation and return values are declared in a React reducer function. A reducer function generally includes all actions related to a particular type of an application entity. For example, the following `postsReducer` function defines a couple of action `type`s for a `post` entity: + +```js +export function postsReducer(posts, action) { + const { type, payload } = action; + const { id } = payload; + + switch (type) { + // highlight-next-line + case "like": + return posts.map((p) => { + if (p?.id === id) { + return { + ...p, + likes: p?.likes + 1, + }; + } else { + return p; + } + }); + + // highlight-next-line + case "unlike": + return posts.map((p) => { + if (p?.id === id) { + return { + ...p, + likes: p?.likes <= 0 ? p?.likes : p?.likes - 1, + }; + } else { + return p; + } + }); + + default: + return posts; + } +} +``` + +The two actions we have are `like` and `unlike`, which are represented as two cases of the `switch` statement. + +Notice, the reducer function takes in the `posts` state, along with an `action` as arguments. The passed in `posts` state represents the current state. It works on the current state according to the data sent with the `action` argument and sends out a new `posts` version. In other words, it acts directly on the state and returns an updated version. + +Notice also that, the `action` consists of a `type` property and any specifics that describe the action: such as a `payload` with `id` and related data. + +The initial state of this reducer must be present before hand. With `useReducer()`, state is initialized with the `initialState` or an `initializer` function as argument. More on this [here](#react-usereducer-passing-initial-state). + +## React State Manipulation with useReducer: A Demo Posts App + +In this section, we delve into the implementation of reducers and actions with React `useReducer()` hook in a demo Posts app. The app allows `like` and `unlike` actions on a post. + +### Starter Files + +The demo app is available in the repository [here](https://github.com/anewman15/hooks-user). Please feel free to get a copy of the `main` branch and run it locally by following these steps: + +1. Clone [this repository](https://github.com/anewman15/hooks-user) to a directory of your choice. +2. Navigate to the app folder and run the server: + +```bash +cd hooks-user +npm install +npm run start +``` + +This will get the app running on `http://localhost:3000`. + +Please feel free to play around with the number of likes on each post, by liking and unliking them. Navigate around the code in your editor and try to make sense of what's going on. + +### The `` Component + +The main action is happening in the `` component. It looks like below: + +
+ +Show `` component + +```js +import React, { useReducer } from "react"; +import { postsReducer, initialPosts } from "../reducers/postsReducer"; +import { + HandThumbDownIcon, + HandThumbUpIcon, + HeartIcon, +} from "@heroicons/react/24/outline"; + +export default function Posts() { + // highlight-next-line + const [posts, dispatch] = useReducer(postsReducer, initialPosts); + + return ( +
+
+

All Posts

+ {posts?.map((post) => { + return ( +
+
+
+
+
+
+ {post?.title} +
+
{post?.subtitle}
+
+
+ + + {post?.likes} + +
+
+
+

+ {post?.content} +

+
+ + +
+
+
+ ); + })} +
+
+ ); +} +``` + +
+ +The JSX presents a list of post items in the `posts` array, by mapping each item into a card. Typical React stuff. Notice inside the ` + + + ); +} +``` + + + +Notice, we pass the `posts` array and the `dispatch` function to `` and use them inside the JSX where necessary. A `create` action is dispatched with form data when the form is submitted: + +```js +const handleSubmit = (e) => { + e.preventDefault(); + // highlight-next-line + dispatch({ type: "create", payload: postFormData }); + setPostFormData(initialFormData); +}; +``` + +For each post in ``, we'll add a `delete` button that dispatches the `delete` action type: + +```js + +``` + +We'll add `` inside ``. So update the `` component to below: + +
+ +Show updated `` component code + +```js +import React, { useReducer } from "react"; +// highlight-next-line +import { CreatePostForm } from "../components/create-post-form"; +import { postsReducer, initialPosts } from "../reducers/postsReducer"; +import { + HandThumbDownIcon, + HandThumbUpIcon, + HeartIcon, +} from "@heroicons/react/24/outline"; + +export default function Posts() { + const [posts, dispatch] = useReducer(postsReducer, initialPosts); + + return ( +
+
+ // highlight-start +
+ +
+ // highlight-end +
+

All Posts

+ {posts?.map((post) => { + return ( +
+
+
+
+
+
+ {post?.title} +
+
{post?.subtitle}
+
+
+ + + {post?.likes} + +
+
+
+

+ {post?.content} +

+
+
+ +
+
+ + +
+
+
+
+ ); + })} +
+
+
+ ); +} +``` + +
+ +With these changes, we should now be able to use the form fields to create a post and use the `delete` button to delete one. + +### React useReducer: Using an Initializer Function + +In the demo, we have passed the initial state of `posts` as a pre-declared array of objects. However, in a real application, the initial state comes from an API typically with a `fetch` call or any other third party library such as Axios or React Query. + +So, we can expect the initial values to yield from an `async` function. In such a scenario, it is possible to pass an initial state by invoking the `async` function with necessary arguments. For example, like the `getInitialPosts` function below: + +```js +const getInitialPosts = async (url) => getPosts(url); + +// highlight-next-line +const [posts, dispatch] = useReducer( + postsReducer, + await getInitialPosts("/posts"), +); +// Invoking is not ideal +``` + +However, invoking an initializer is suboptimal, since `userReducer`'s initial state gets evaluated only during the reducer's initialization. And it does not get re-evaluated on state updates and subsequent renders. In other words re-evaluation of state initializer function is ignored despite it being called every time the component re-renders. + +So, using an invoked initilizer function with `useReducer()` wastes application resources. + +We can instead pass the function itself as the third argument to `useReducer()`, and any of its own arguments as the second argument to `useReducer()`. Like so: + +```js +const [posts, dispatch] = useReducer(postsReducer, "/posts", getInitialPosts); +// Initializer function passed as third argument, without being invoked +// Any argument of initializer function passed as second argument to `useReducer()` +``` + +This way, the initializer function (`getInitialPosts` here) passed its dependent arguments (`/posts` url) on the reducer's initialization. The initializer function now only gets invoked during that time. Any subsequent call is prevented. This helps optimize the component. + +## React useReducer: Benefits and Comparison + +In this section, we discuss the benefits of using `useReducer()` hook and how it compares to Redux which also uses reducers under the hood. + +### Benefits of Using React useReducer Hook + +Major benefits of using the `useReducer()` hook in React are: + +- Helps consolidate related states in one place. +- Helps implement multiple actions on an application entity. +- Reducer state and functions can be refactored outside of a component. +- Reducer state and functions are reusable from multiple components. +- Reducer state and functions are reinitializable with different states for different components. +- The `dispatch` API allows us derive individual functions for handling action types. + +### React useReducer Compared with Redux + +Reducers form the backbone of [Redux](https://redux.js.org/introduction/getting-started) which is a lightweight client side state management solution for React. The idea of consolidating states into one is essential in both Redux and `useReducer()`, as well as the notion of action types and dispatching them with the `dispatch()` API. In addition, the structure of the reducer function is the same in both Redux and `useReducer()`. + +However, Redux is tailored for managing client state globally, often over a vast majority of components in a React app. It uses a global store composed of individual reducer slices. Each slice would have its own reducer function, state and action types. And slices are combined to make up a global store. In contrast, `useReducer()` implements standalone reducers, which compare as single slices in a Redux store. + +## Summary + +In this post, explored with a demo app, how to use the React `userReducer()` for consolidating component state into a reducer. We learned that `useReducer()` initializes a React reducer with state and `dispatch()` function for invoking actions on the state. We followed examples from a `postsReducer` function to understand what constitutes a reducer function and discussed the good practices involved while writing efficient reducer function. We also explored how `useReducer()` with a reducer helps implement state actions at multiple levels of its nesting. + +Towards the end, we outlined the benefits of using `useReducer()` and learned how it compares to global use of reducers in Redux.