Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
August 4, 2020 10:20 am GMT

A Practical Introduction to Using Redux with React

Table of Contents

  1. Introduction
  2. What is Redux?
  3. What is the State?
  4. How to Modify the State?
  5. Unidirectional Data Flow
  6. Setting Up Redux in a React App
  7. Using React Hooks to Read the State
  8. Using React Hooks to Dispatch Actions
  9. Using "json-server" for the Local Fake API
  10. Async Actions
  11. Multiple Reducers
  12. Feature Folders and Ducks
  13. Using the "Ducks" Pattern in Our Example App

Introduction

In this tutorial I'd like to explain briefly what is Redux and how to set it up in a React project.

This tutorial would be useful for you if you've learned React already and would like to learn how Redux can help managing the app's state globally.

What is Redux?

Redux is a state management library. Commonly, it is used together with React, but it can be used with other view libraries too.
Redux helps us to keep the state of the whole app in a single place.

What is the State?

I would describe "state" as the data that is used to render the app at any given time. We keep this data in a JavaScript object. For example, in a simple app which renders a list of muffins, the state could look like this:

let state = {  muffins: [    { name: 'Chocolate chip muffin' },    { name: 'Blueberry muffin' }  ]}

How to Modify the State?

To modify the state from within a component we dispatch an action:

// SomeComponent.jsdispatch({  type: 'muffins/add',  payload: {    muffin: { name: 'Banana muffin' },  },});

Dispatching actions is the only way to change the state.

An action is represented by an object with the type property. The type property is the action's name. You can add any other property to this object (this is how you pass the data to reducer).

There are no formal rules as to how you should name your actions. Give your actions descriptive and meaningful names. Don't use ambiguous names, like receive_data or set_value.

It is a common practice to share actions through the action creator functions. Such functions create and return the action objects. We store action creators outside of the component files (e.g., src/redux/actions.js). This makes it easy to see what actions are available in the app and to maintain and reuse them.

// actions.jsexport function addMuffin(muffin) {  return {    type: 'muffins/add',    payload: { muffin },  };}// SomeComponent.jsdispatch(addMuffin({ name: 'Banana muffin' }));

Once an action is dispatched, Redux calls the reducer with the previous state and the dispatched action object as the arguments. Reducer is a function which decides how to change the state according to a given action. We create this function and register it with Redux.

This is how a basic reducer looks like:

let initialState = {  muffins: [    { id: 1, name: 'Chocolate chip muffin' },    { id: 2, name: 'Blueberry muffin' },  ],};function reducer(state = initialState, action) {  switch (action.type) {    case 'muffins/add':      let { muffin } = action.payload;      return { ...state, muffins: [...state.muffins, muffin] };    default:      return state;  }}

When this reducer identifies the muffins/add action it adds the given muffin to the list.

IMPORTANT. The reducer copies the previous state object instead of mutating it. The rule is that the state must be immutable (read-only). The reducer should copy any object that it would like to change before changing it. This includes the root object and any nested objects.

We need to copy the state for Redux to be able to check (using shallow checking) if the state returned by the reducer is different from the previous state. Check this for more details about shallow checking: How do shallow and deep equality checking differ?. It is important to follow this rule for Redux to respond to our state changes correctly. Also, when using redux with react-redux this helps react-redux to decide which components should be re-rendered when the state changes.

The other important rule is that the reducer function should be pure. Given the same input it should always produce the same output without causing any side effects. A side affect is something that reads or changes the environment around the function. Examples of side effects are reading or writing a global variable, running a network request, etc. This rule helps us reproduce the look and behavior of the app given a particular state object.

Also, both of these rules make sure that Redux's time travel feature works properly with our app. Time travel allows us to easily undo actions and then apply them again. This helps a lot with debugging using Redux DevTools.

To summarize:

  • Our app has a single state.
  • To change this state we dispatch actions.
  • The reducer function handles the dispatched actions and changes the state accordingly.
  • Redux and react-redux check the state returned by the reducer for changes using shallow checking.

Unidirectional Data Flow

So, we've learned the following about Redux: we dispatch an action from the view layer (e.g., a React component), reducer gets this action and changes the state accordingly, the store notifies the view layer about the state change and the view layer renders the app according to the latest state. And the cycle repeats when we need to change the state again.

So, the data in a Redux app flows in a single way circular pattern. It is also called a unidirectional data flow. This is how we could represent it using a diagram:

redux-data-flow

This pattern makes it easier to understand how a Redux app works.

Setting Up Redux in a React App

In this post we will be building a simple app which lists a number of muffins.

I've initialized a basic React app using create-react-app:

npx create-react-app my-react-redux

I removed extra code and rendered a hard-coded list of muffins. This is what I've got: View on GitHub

Let's go ahead and store the muffins in the state.

First, let's install "redux" and "react-redux" packages:

npm i -S redux react-redux

Remember, Redux can be used with other view libraries. So we need the "react-redux" package to connect React components with Redux store.

Next, we should prepare the Redux store. The store is an object which keeps the app's state and provides the API for working with it. It allows us to:

  • read the state
  • dispatch actions to change the state
  • and subscribe/unsubscribe to/from the state changes

IMPORTANT. Your app should have a single store.

Let's go ahead and set up the store for our example app.

Let's keep the Redux functionality in the folder called "redux":

mkdir src/redux

Let's write the store initialization code in the file src/redux/store.js:

// File: src/redux/store.jsimport { createStore } from 'redux';const initialState = {  muffins: [    { id: 1, name: 'Chocolate chip muffin' },    { id: 2, name: 'Blueberry muffin' },  ],};const reducer = (state = initialState, action) => {  switch (action.type) {    default:      return state;  }};const store = createStore(reducer);export default store;

We use the createStore function from the redux package to create the store. When the store initializes, it obtains the initial state by calling our reducer function with undefined for the state and a dummy action (e.g., reducer(undefined, { type: 'DUMMY' })).

Now we should provide the store to the React components.
For this, we open the src/index.js and wrap the <App /> component into the <Provider /> component from the "react-redux" package:

import React from 'react';import ReactDOM from 'react-dom';import { Provider } from 'react-redux';import './index.css';import App from './components/App';import store from './redux/store';ReactDOM.render(  <Provider store={store}>    <App />  </Provider>,  document.getElementById('root'));

The <Provider /> component provides the store to the child component tree using React context. Now we can use the React hooks or the connect function from the "react-redux" package to get the state and dispatch actions from any component in the tree.

View the code on GitHub

Using React Hooks to Read the State

Instead of hard-coding the muffin list in the "Muffins.js", let's use the useSelector hook from "react-redux" to select the muffins array from the state.

// file: src/redux/selectors.jsexport const selectMuffinsArray = (state) => state.muffins;
// file: src/components/Muffins/Muffins.jsimport React from 'react';import { useSelector } from 'react-redux';import { selectMuffinsArray } from '../../redux/selectors';const Muffins = () => {  const muffins = useSelector(selectMuffinsArray);  return (    <ul>      {muffins.map((muffin) => {        return <li key={muffin.id}>{muffin.name}</li>;      })}    </ul>  );};export default Muffins;

The useSelector hook expects a selector function as the first argument. We create selector functions to provide a reusable API for selecting different parts of the state.

We use the state in many components. If we select things from the state directly (e.g. let muffins = state.muffins) and at some point we change the structure of the state (e.g., state.muffins becomes state.muffins.items) we'd have to edit each component where we access the state properties directly. Using selector functions we can change the way we select the state in a single place (in our example, it is the "selectors.js" file).

View the code on GitHub

Using React Hooks to Dispatch Actions

Let's add a "Like" button to each muffin in the list.

First, let's add the "likes" property to the state (number of likes).

// file: src/redux/store.jsconst initialState = {  muffins: [    { id: 1, name: 'Chocolate chip muffin', likes: 11 },    { id: 2, name: 'Blueberry muffin', likes: 10 },  ],};

Next, let's render the number of likes and the "Like" button.

// file: src/components/Muffins/Muffins.js<li key={muffin.id}>  {muffin.name} <button>Like</button> <i>{muffin.likes}</i></li>

Now, let's get the dispatch function in the component using the useDispatch hook from "react-redux".

// file: src/components/Muffins/Muffins.jsimport { useSelector, useDispatch } from 'react-redux';// ...const dispatch = useDispatch();

Let's define an action for the "Like" button.

// File: src/redux/actions.jsexport const likeMuffin = (muffinId) => ({  type: 'muffins/like',  payload: { id: muffinId },});

Next, let's create the "click" event handler for the "Like" button:

// {"lines": "2,4-9,12"}// file: src/components/Muffins/Muffins.jsimport { likeMuffin } from '../../redux/actions';// ...{  muffins.map((muffin) => {    const handleLike = () => {      dispatch(likeMuffin(muffin.id));    };    return (      <li key={muffin.id}>        {muffin.name} <button onClick={handleLike}>Like</button>{' '}        <i>{muffin.likes}</i>      </li>    );  });}

If we click this button, nothing happens, because we didn't create a reducer for the action that is dispatched (muffins/like).

So, let's go ahead and reduce this action.

// {"lines": "4-14"}// file: src/redux/store.jsconst reducer = (state = initialState, action) => {  switch (action.type) {    case 'muffins/like':      const { id } = action.payload;      return {        ...state,        muffins: state.muffins.map((muffin) => {          if (muffin.id === id) {            return { ...muffin, likes: muffin.likes + 1 };          }          return muffin;        }),      };    default:      return state;  }};

It's important NOT to mutate the state. So, I copy the state object, copy the muffins array (the map method returns a new array). Finally, I copy only the muffin which is being changed. I don't touch the other muffins to signify that they do not change.

Now, if we click the "Like" button, the muffins/like action is dispatched and the reducer changes the state accordingly. The number of likes of the chosen muffin increments.

View the code on GitHub

Using "json-server" for the Local Fake API

"json-server" is a fake REST API server which is really easy to set up. We can use it to mock API endpoints while working on a front end app. I'd like to use this server for the examples in this post. So let me show you how to install and run it.

To install:

npm i -D json-server

To tell the server what data it should serve we create a JSON file. Let's call it db.json.

{  "muffins": [    { "id": 1, "name": "Chocolate chip muffin", "likes": 11 },    { "id": 2, "name": "Blueberry muffin", "likes": 10 }  ]}

Now let's open package.json and add the script which will start this server:

// {"lines": "2"}"scripts": {  "json-server": "json-server --watch db.json --port 3001"}

To run it:

npm run json-server

The server should start on http://localhost:3001.

To stop it, focus on the terminal window where you started it and press CTRL + C.

We can use the following routes ("json-server" generates them by looking at db.json)

GET /muffinsPOST /muffinsPUT /muffins/{id}DELETE /muffins/{id}

View the code on GitHub

Async Actions

Please check the section Using "json-server" for the Local Fake API.

Usually, we run network requests to get and edit the data. Let's see how to do it the Redux way.

By default, Redux allows us to dispatch an action only in the form of an object with the type property.

However, Redux allows us to alter the way it dispatches actions using a middleware function. One such function is called "redux-thunk".

Let's install and register this middleware function with Redux.

npm i -S redux-thunk
// file: src/redux/store.jsimport { createStore, applyMiddleware } from 'redux';import thunk from 'redux-thunk';// ...const store = createStore(reducer, applyMiddleware(thunk));

applyMiddleware is a utility function which takes a list of middleware functions and groups them in a single middleware function which we pass to createStore as the second argument.

Also, let's empty the muffins array in the initial state, because we are going to load muffins from the fake API.

// file: src/redux/store.jsconst initialState = {  muffins: [],};

"redux-thunk" allows us to dispatch not only objects, but also functions:

dispatch((dispatch, getState) => {  let state = getState();  // do something async and  dispatch(/* some action */);});

The thunk function gets the original dispatch function as the first argument and the getState function as the second argument.

So, what we can do with a thunk function is, for example, to fetch the data from the network and when the data is ready we can dispatch an action object with this data, so reducer can add this data to the state.

Let's create the actions.js file and add the async action creator function for loading muffins.

// file: src/redux/actions.jsexport const loadMuffins = () => async (dispatch) => {  dispatch({    type: 'muffins/load_request',  });  try {    const response = await fetch('http://localhost:3001/muffins');    const data = await response.json();    dispatch({      type: 'muffins/load_success',      payload: {        muffins: data,      },    });  } catch (e) {    dispatch({      type: 'muffins/load_failure',      error: 'Failed to load muffins.',    });  }};

A thunk function can be either sync or async. We can dispatch multiple actions in this function. In our example we dispatch the muffins/load_request action to signify that the request starts. We can use this action to show a spinner somewhere in the app. Then, when the request succeeds we dispatch the muffins/load_success action with the fetched data. Finally, if the request fails, we dispatch the muffins/load_failure action to show the error message to the user.

Now, let's create the reducers for these actions.

// file: src/redux/store.jsconst reducer = (state = initialState, action) => {  switch (action.type) {    // ...    case 'muffins/load_request':      return { ...state, muffinsLoading: true };    case 'muffins/load_success':      const { muffins } = action.payload;      return { ...state, muffinsLoading: false, muffins };    case 'muffins/load_failure':      const { error } = action;      return { ...state, muffinsLoading: false, error };    // ...  }};

Let's dispatch the loadMuffins action in the Muffins component, when it mounts.

// file: src/components/Muffins/Muffins.jsimport React, { useEffect } from 'react';import { loadMuffins } from '../../redux/actions';// ...const dispatch = useDispatch();useEffect(() => {  dispatch(loadMuffins());}, []);

We are loading muffins in the effect hook, because dispatching an action is a side effect.

Finally, let's handle the loading and error states.

Create the following selector functions:

// file: src/redux/selectors.jsexport const selectMuffinsLoading = (state) => state.muffinsLoading;export const selectMuffinsLoadError = (state) => state.error;

And render the loading and error messages:

// file: src/components/Muffins/Muffins.jsconst muffinsLoading = useSelector(selectMuffinsLoading);const loadError = useSelector(selectMuffinsLoadError);// ...return muffinsLoading ? (  <p>Loading...</p>) : loadError ? (  <p>{loadError}</p>) : muffins.length ? (  <ul>    {muffins.map((muffin) => {      // ...    })}  </ul>) : (  <p>Oh no! Muffins have finished!</p>);

Now, let's check if we did everything correctly.

We should run the local "json-server" and the app.

In one terminal window:

npm run json-server

And in the other:

npm start

In the browser you should see the list of muffins which is, now, fetched from the fake API server.

View the code on GitHub

Multiple Reducers

Usually, in a large app, state won't be that simple. It will look like a huge tree of data.

The reducer function will become bloated.

So, it's a good idea to split the reducer into multiple smaller reducers where each reducer handles only a part of the state.

For example, in order to handle the state from the picture above, it would be a good idea to create 3 reducers:

const muffinsReducer = (state = initialMuffinsState, action) => {  // ...};const notificationsReducer = (state = initialNotificationsState, action) => {  // ...};const cartReducer = (state = initialCartState, action) => {  // ...};

and combine them using the utility function called combineReducers:

const rootReducer = combineReducers({  muffins: muffinsReducer,  notifications: notificationsReducer,  cart: cartReducer,});const store = createStore(rootReducer);

combineReducers creates a root reducer function which calls each sub reducer when the action is dispatched and combines the parts of the state they return into a single state object:

{  muffins: ...,  notifications: ...,  cart: ...}

Combining reducers makes it easy to modularize the reducer logic.

Feature Folders and Ducks

The Redux documentation recommends structuring Redux functionality as feature folders or ducks.

Feature Folders

Instead of grouping all actions and reducers by the type of code (for example, all the app's actions in actions.js and all reducers in reducers.js), we could group them by feature.

Let's say there are two features: "users" and "notifications". We could keep their actions and reducers in separate folders. For example:

redux/  users/    actions.js    reducers.js  notifications/    actions.js    reducers.js  store.js

Ducks

The "ducks" pattern says that we should keep all Redux logic (actions, reducers, selectors) for a specific feature in its own file. For example:

redux/  users.js  notifications.js  store.js

Using the "Ducks" Pattern in Our Example App

In the app we've got different Redux functionality around muffins. We can group this functionality into a duck. In other words, let's just move everything related to mufffins into a JavaScript file and call it src/redux/muffins.js.

Let's move the actions, selectors and the reducer to this file:

export const likeMuffin = (muffinId) => ({  type: 'muffins/like',  payload: { id: muffinId },});export const loadMuffins = () => async (dispatch) => {  dispatch({    type: 'muffins/load_request',  });  try {    const response = await fetch('http://localhost:3001/muffins');    const data = await response.json();    dispatch({      type: 'muffins/load_success',      payload: {        muffins: data,      },    });  } catch (e) {    dispatch({      type: 'muffins/load_failure',      error: 'Failed to load muffins.',    });  }};export const selectMuffinsArray = (state) => state.muffins;export const selectMuffinsLoading = (state) => state.muffinsLoading;export const selectMuffinsLoadError = (state) => state.error;const initialState = {  muffins: [],};const reducer = (state = initialState, action) => {  switch (action.type) {    case 'muffins/like':      const { id } = action.payload;      return {        ...state,        muffins: state.muffins.map((muffin) => {          if (muffin.id === id) {            return { ...muffin, likes: muffin.likes + 1 };          }          return muffin;        }),      };    case 'muffins/load_request':      return { ...state, muffinsLoading: true };    case 'muffins/load_success':      const { muffins } = action.payload;      return { ...state, muffinsLoading: false, muffins };    case 'muffins/load_failure':      const { error } = action;      return { ...state, muffinsLoading: false, error };    default:      return state;  }};export default reducer;

Now, in the src/redux/store.js, let's create the root reducer using the combineReducers function:

// {"lines": "6-10"}// File: src/redux/store.jsimport { createStore, applyMiddleware, combineReducers } from 'redux';import thunk from 'redux-thunk';import muffinsReducer from './muffins';const rootReducer = combineReducers({  muffins: muffinsReducer,});const store = createStore(rootReducer, applyMiddleware(thunk));export default store;

Now, the app's state looks like this:

{  muffins: {    muffins: [],    muffinsLoading: boolean,    error: string  }}

Since the structure of the state has changed, to make the app work, we should update the parts of the code where we read the state. Luckily, we use selector functions to select parts of the state object instead of working with the state object directly. So, we only have to update the selector functions:

// File: src/redux/muffins.jsexport const selectMuffinsState = (rootState) => rootState.muffins;export const selectMuffinsArray = (rootState) =>  selectMuffinsState(rootState).muffins;export const selectMuffinsLoading = (rootState) =>  selectMuffinsState(rootState).muffinsLoading;export const selectMuffinsLoadError = (rootState) =>  selectMuffinsState(rootState).error;

Finally, let's update the import statements:

// {"lines": "6,7"}// File: src/components/Muffins/Muffins.jsimport {  selectMuffinsArray,  selectMuffinsLoading,  selectMuffinsLoadError,} from '../../redux/muffins';import { likeMuffin, loadMuffins } from '../../redux/muffins';

That's it! We used the "ducks" pattern to move the Redux functionality around managing the muffins state into a single file.

View the code on GitHub


Original Link: https://dev.to/ddmytro/a-practical-introduction-to-using-redux-with-react-1a0m

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To