Як використовувати Redux у ReactJS із реальними прикладами

Оскільки я почав працювати з ReactJS, у Creative-Tim я використовував його лише для створення простих програм для реагування чи шаблонів, якщо хочете. Я використовував ReactJS лише з create-response-app і ніколи не намагався інтегрувати його з чимось більше.

Багато наших користувачів запитували мене або мою команду, чи створені мною шаблони мали Redux. Або якщо вони були створені таким чином, щоб їх можна було використовувати з Redux. І моєю відповіддю завжди було щось на кшталт: «Я ще не працював з Redux і не знаю, яку відповідь я повинен вам дати».

Отож я зараз пишу статтю про Redux і про те, як його слід використовувати в React. Пізніше, у цій статті, я збираюся додати Redux поверх одного з проектів, над яким я працював протягом останнього та декількох років.

Це добре знати, перш ніж продовжувати боротьбу з цими двома бібліотеками:

  • Я збираюся використовувати [email protected] (встановлений глобально)
  • Я використовую [email protected]
  • Моя версія Node.js на момент написання цього допису була 10.13.0 (LTS)
  • Якщо ви хочете використовувати Webpack замість цього, тоді ви можете прочитати мою статтю Webpack і поєднати те, що я вам там показую, з тим, що я вам тут покажу.

Створення нового проекту на основі ReactJS та додавання до нього Redux

Перш за все давайте створимо нову програму для реагування, компакт-диск і запустімо її.

create-react-app react-redux-tutorial cd react-redux-tutorial npm start

Як ми бачимо, додаток create-response-додає нам дуже простий шаблон з абзацом, прив'язкою до веб-сайту React та офіційною піктограмою ReactJS, що обертається.

Я не казав вам, хлопці, для чого ми будемо використовувати Redux і що ми тут робимо. І це тому, що мені знадобилося вищезазначене зображення gif.

Щоб зробити цей підручник легким і зрозумілим, ми не збираємося будувати щось дуже складне. Ми збираємося використовувати Redux, щоб згадане зображення React зупинилося або почало обертатися.

Отже, сказавши, давайте додамо наступні пакети Redux :

npm install --save redux react-redux

redux v4.0.1

  • Що Redux робить у дуже загальному сенсі, так це те, що він створює глобальний стан для всієї програми, до якої може отримати доступ будь-який ваш компонент
  • Це державна бібліотека управління
  • У вас є лише один стан для всього вашого додатка, а не штати для кожного з ваших компонентів

реакція-відновлення v5.1.1

  • Це використовується, щоб ми могли отримати доступ до даних Redux і змінити їх, надсилаючи дії Redux - насправді не Redux, але ми дійдемо туди
  • Офіційні документи стверджують: це дозволяє вашим компонентам React читати дані з магазину Redux і відправляти дії до магазину для оновлення даних

ПРИМІТКА . Якщо у вас є проблеми з наведеною командою, спробуйте встановити пакети окремо

Працюючи з Redux, вам знадобляться три основні речі:

  • дії: це об’єкти, які повинні мати дві властивості: одна, що описує тип дії, а інша, що описує, що слід змінити в стані програми.
  • редуктори: це функції, що реалізують поведінку дій. Вони змінюють стан програми на основі опису дії та опису змін стану.
  • store: він об’єднує дії та редуктори, утримуючи та змінюючи стан для всієї програми - існує лише один магазин.

Як я вже говорив вище, ми зупинимось і почнемо крутити логотип React. Це означає, що нам знадобляться дві дії наступним чином:

1 - Команди Linux / Mac

mkdir src/actions touch src/actions/startAction.js touch src/actions/stopAction.js

2 - Команди Windows

mkdir src\actions echo "" > src\actions\startAction.js echo "" > src\actions\stopAction.js

Тепер відредагуємо src / actions / startAction.js таким чином:

export const startAction = { type: "rotate", payload: true };

Отже, ми збираємось сказати нашому редуктору, що тип дії стосується обертання ( обертання ) логотипу React. І стан для повороту логотипу React слід змінити на true - ми хочемо, щоб логотип почав обертатися.

Тепер відредагуємо src / actions / stopAction.js таким чином:

export const stopAction = { type: "rotate", payload: false };

Отже, ми збираємось сказати нашому редуктору, що тип дії стосується обертання ( обертання ) логотипу React. А стан повороту логотипу React слід змінити на false - ми хочемо, щоб логотип перестав обертатися.

Давайте також створимо редуктор для нашого додатку:

1 - Команди Linux / Mac

mkdir src/reducers touch src/reducers/rotateReducer.js

2 - Команди Windows

mkdir src\reducers echo "" > src\reducers\rotateReducer.js

І, додайте в нього наступний код:

export default (state, action) => { switch (action.type) { case "rotate": return { rotating: action.payload }; default: return state; } };

Отже, редуктор отримає обидві наші дії, обидві з яких мають тип повороту, і обидва вони змінюють однаковий стан у програмі - тобто state.rotating . Залежно від корисного навантаження цих дій, state.rotating зміниться на true або false .

Я додав регістр за замовчуванням , який зберігатиме стан незмінним, якщо тип дії не обертається . Значення за замовчуванням є на випадок, якщо ми створимо дію та забудемо додати регістр для цієї дії. Таким чином, ми не видаляємо весь стан програми - ми просто нічого не робимо і зберігаємо те, що мали.

Останнє, що нам потрібно зробити, це створити наш магазин для всього додатка. Оскільки для всього додатка існує лише один магазин / один стан, ми не збираємося створювати нову папку для магазину. Якщо ви хочете, ви можете створити нову папку для магазину і додати її туди, але це не так, як з діями, наприклад, де ви можете робити кілька дій, і виглядає краще, щоб тримати їх всередині папки.

Отже, сказано, що ми будемо запускати цю команду:

1 - Команда Linux / Mac

touch src/store.js

2 - команда Windows

echo "" > src\store.js

А також додайте в нього наступний код:

import { createStore } from "redux"; import rotateReducer from "reducers/rotateReducer"; function configureStore(state = { rotating: true }) { return createStore(rotateReducer,state); } export default configureStore;

Отже, ми створюємо функцію з іменем configureStore, в якій ми надсилаємо стан за замовчуванням, і створюємо наш магазин, використовуючи створений редуктор і стан за замовчуванням.

Я не впевнений, що ви бачили мій імпорт, вони використовують абсолютні шляхи, тому через це у вас можуть бути помилки. Виправлення цього є одним із двох:

Або

1 - Додайте файл .env у свій додаток приблизно так:

echo "NODE_PATH=./src" > .env

Або

2 - Встановіть cross-env глобально та змініть стартовий сценарій із файлу package.json приблизно так:

npm install -g cross-env

А всередині package.json

"start": "NODE_PATH=./src react-scripts start",

Тепер, коли ми створили наш магазин, наші дії та наш редуктор, нам потрібно додати новий клас всередині файлу src / App.css . Цей клас призупинить обертання анімації логотипу.

Отже, ми збираємося написати всередині src / App.css :

.App-logo-paused { animation-play-state: paused; }

So your App.css file should look something like this:

.App { text-align: center; } .App-logo { animation: App-logo-spin infinite 20s linear; height: 40vmin; } /* new class here */ .App-logo-paused { animation-play-state: paused; } .App-header { background-color: #282c34; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10px + 2vmin); color: white; } .App-link { color: #61dafb; } @keyframes App-logo-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }

Now, we only need to modify our src/App.js file so that it listens to our store state. And when clicking on the logo, it calls one of the start or stop actions.

First things first, we need to connect our component to our redux store so we import connect from react-redux.

import { connect } from "react-redux";

After this, we’ll export our App component through the connect method like this:

export default connect()(App);

To change the redux store state we’ll need the actions that we’ve done earlier so let’s import them as well:

import { startAction } from "actions/startAction"; import { stopAction } from "actions/stopAction";

Now we need to retrieve the state from our store and to say that we want the start and stop actions to be used for changing the state.

This will be done using the connect function, which accepts two parameters:

  • mapStateToProps: this is used to retrieve the store state
  • mapDispatchToProps: this is used to retrieve the actions and dispatch them to the store

You can read more about them here: react-redux connect function arguments.

So let’s write inside our App.js (at the end of the file if you may):

const mapStateToProps = state => ({ ...state }); const mapDispatchToProps = dispatch => ({ startAction: () => dispatch(startAction), stopAction: () => dispatch(stopAction) });

After this, let’s add them inside our connect function like so:

export default connect(mapStateToProps, mapDispatchToProps)(App);

And right now, inside our App component, we can access the store state, the startAction and stopAction through props.

Let’s change the img tag to:

So, what we are saying here is, if the store state of rotating (this.props.rotating) is true, then we want just the App-logoclassName to be set to our img. If that is false, then we also want the App-logo-paused class to be set in the className. This way we pause the animation.

Also, if this.props.rotating is true, then we want to send to our store for the onClick function and change it back to false, and vice-versa.

We are almost done, but we’ve forgot something.

We haven’t yet told our react app that we have a global state, or if you will, that we use redux state management.

For this, we go inside src/index.js, we import a Provider from react-redux, and the newly created store like so:

import { Provider } from "react-redux"; import configureStore from "store";
  • Provider: makes the Redux store available to any nested components that have been wrapped in the connect function

After this, instead of rendering our App component directly, we render it through our Provider using the store that we’ve created like so:

ReactDOM.render(   , document.getElementById('root') );

Here we could have used the configureStore function with some other state, for example configureStore({ rotating: false }).

So, your index.js should look like this:

import React from 'react'; import ReactDOM from 'react-dom'; // new imports start import { Provider } from "react-redux"; import configureStore from "store"; // new imports stop import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; // changed the render ReactDOM.render(   , document.getElementById('root') ); // changed the render serviceWorker.unregister();

Let’s go ahead and see if our redux app works:

Using action creators

Optionally, instead of actions, we can use action creators, which are functions that create actions.

This way, we can combine our two actions in just one function and reduce a bit our code.

So, let’s go ahead and create a new file:

1 — Linux / Mac command

touch src/actions/rotateAction.js

2 — Windows command

echo "" > src\actions\rotateAction.js

And add this code:

const rotateAction = (payload) => { return { type: "rotate", payload } } export default rotateAction;

We are going to send an action of type rotate, with a payload that we are going to get in the App component.

Inside the src/App.js component, we need to import our new action creator:

import rotateAction from "actions/rotateAction";

Add the new function to the mapDispatchToProps like so:

rotateAction: will receive a (payload) and will dispatch the rotateAction with the payload

Change the onClick function to:

onClick={() => this.props.rotateAction(!this.props.rotating)}

And finally, add our new action creator to the mapDispatchToProps like this:

rotateAction: (payload) => dispatch(rotateAction(payload))

We can also delete the old imports for the old actions, and delete them from the mapDispatchToProps as well.

This is how you new src/App.js should look like:

import React, { Component } from 'react'; // new lines from here import { connect } from "react-redux"; import rotateAction from "actions/rotateAction"; //// new lines to here import logo from './logo.svg'; import './App.css'; class App extends Component { render() { console.log(this.props); return (  this.props.rotateAction(!this.props.rotating) } />

Edit src/App.js and save to reload.

Learn React ); } } const mapStateToProps = state => ({ ...state }); const mapDispatchToProps = dispatch => ({ rotateAction: (payload) => dispatch(rotateAction(payload)) }); export default connect(mapStateToProps, mapDispatchToProps)(App);

A real-life example with Paper Dashboard React

As you will see in the above gif image, I am using the right menu to change the colors of the menu on the left. This is achieved by using component states, and by passing that state from a parent component to the two menus and some functions to change that state.

I thought it would be a nice example, to take this product and replace the component states with Redux.

You can get it in these 3 ways:

  1. Download from creative-tim.com
  2. Download from Github
  3. Clone from Github:
git clone //github.com/creativetimofficial/paper-dashboard-react.git

Now that we have this product, let’s cd into it and install again redux and react-redux:

npm install --save redux react-redux

After this, we need to create the actions. Since in the right menu we have 2 colors that set the background of the left menu, and 5 colors that change the color of the links, we need 7 actions, or 2 actions creators — and we are going with this second option since it is a bit less code to write:

1 — Linux / Mac commands

mkdir src/actions touch src/actions/setBgAction.js touch src/actions/setColorAction.js

2 — Windows commands

mkdir src\actions echo "" > src\actions\setBgAction.js echo "" > src\actions\setColorAction.js

After this, let’s create the actions code as follows:

src/actions/setBgAction.js

const setBgAction = (payload) => { return { type: "bgChange", payload } } export default setBgAction;

src/actions/setColorAction.js

const setColorAction = (payload) => { return { type: "colorChange", payload } } export default setColorAction;

Now, as in the first part, we need the reducer:

1 — Linux / Mac commands

mkdir src/reducers touch src/reducers/rootReducer.js

2 — Windows commands

mkdir src\reducers echo "" > src\reducers\rootReducer.js

And the code for the reducer:

export default (state, action) => { switch (action.type) { case "bgChange": return { ...state, bgColor: action.payload }; case "colorChange": return { ...state, activeColor: action.payload }; default: return state; } };

As you can see here, unlike our first example, we want to keep our old state and update its contents.

We also need the store:

1 — Linux / Mac command

touch src/store.js

2 — Windows command

echo "" > src\store.js

The code for it:

import { createStore } from "redux"; import rootReducer from "reducers/rootReducer"; function configureStore(state = { bgColor: "black", activeColor: "info" }) { return createStore(rootReducer,state); } export default configureStore;

Inside the src/index.js we need:

// new imports start import { Provider } from "react-redux"; import configureStore from "store"; // new imports stop

And also, change the render function:

ReactDOM.render(    {indexRoutes.map((prop, key) => { return ; })}   , document.getElementById("root") );

So the index.js file should look like this:

import React from "react"; import ReactDOM from "react-dom"; import { createBrowserHistory } from "history"; import { Router, Route, Switch } from "react-router-dom"; // new imports start import { Provider } from "react-redux"; import configureStore from "store"; // new imports stop import "bootstrap/dist/css/bootstrap.css"; import "assets/scss/paper-dashboard.scss"; import "assets/demo/demo.css"; import indexRoutes from "routes/index.jsx"; const hist = createBrowserHistory(); ReactDOM.render(    {indexRoutes.map((prop, key) => { return ; })}   , document.getElementById("root") );

Now we need to make some changes inside src/layouts/Dashboard/Dashboard.jsx. We need to delete the state and the functions that change the state. So go ahead and delete these bits of code:

The constructor (between lines 16 and 22):

constructor(props){ super(props); this.state = { backgroundColor: "black", activeColor: "info", } }

The state functions (between lines 41 and 46):

handleActiveClick = (color) => { this.setState({ activeColor: color }); } handleBgClick = (color) => { this.setState({ backgroundColor: color }); }

The sidebar bgColor and activeColor props (lines 53 and 54):

bgColor={this.state.backgroundColor} activeColor={this.state.activeColor}

All of the FixedPlugin props (between lines 59–62):

bgColor={this.state.backgroundColor} activeColor={this.state.activeColor} handleActiveClick={this.handleActiveClick} handleBgClick={this.handleBgClick}

So, we remain with this code inside the Dashboard layout component:

import React from "react"; // javascript plugin used to create scrollbars on windows import PerfectScrollbar from "perfect-scrollbar"; import { Route, Switch, Redirect } from "react-router-dom"; import Header from "components/Header/Header.jsx"; import Footer from "components/Footer/Footer.jsx"; import Sidebar from "components/Sidebar/Sidebar.jsx"; import FixedPlugin from "components/FixedPlugin/FixedPlugin.jsx"; import dashboardRoutes from "routes/dashboard.jsx"; var ps; class Dashboard extends React.Component { componentDidMount() { if (navigator.platform.indexOf("Win") > -1) { ps = new PerfectScrollbar(this.refs.mainPanel); document.body.classList.toggle("perfect-scrollbar-on"); } } componentWillUnmount() { if (navigator.platform.indexOf("Win") > -1) { ps.destroy(); document.body.classList.toggle("perfect-scrollbar-on"); } } componentDidUpdate(e) { if (e.history.action === "PUSH") { this.refs.mainPanel.scrollTop = 0; document.scrollingElement.scrollTop = 0; } } render() { return ( {dashboardRoutes.map((prop, key) => { if (prop.pro) { return null; } if (prop.redirect) { return ; } return (  ); })} ); } } export default Dashboard;

We need to connect the Sidebar and FixedPlugin components to to the store.

For src/components/Sidebar/Sidebar.jsx:

import { connect } from "react-redux";

And change the export to:

const mapStateToProps = state => ({ ...state }); export default connect(mapStateToProps)(Sidebar);

For the src/components/FixedPlugin/FixedPlugin.jsx:

import { connect } from "react-redux"; import setBgAction from "actions/setBgAction"; import setColorAction from "actions/setColorAction";

And the export should now be:

const mapStateToProps = state => ({ ...state }); const mapDispatchToProps = dispatch => ({ setBgAction: (payload) => dispatch(setBgAction(payload)), setColorAction: (payload) => dispatch(setColorAction(payload)) }); export default connect(mapStateToProps, mapDispatchToProps)(FixedPlugin);

We are going to have these next changes:

  • anywhere you find the word handleBgClick, you’ll need to change it to setBgAction
  • anywhere you find the word handleActiveClick, you’ll need to change it to setColorAction

So, the FixedPlugin component should now look like this:

import React, { Component } from "react"; import { connect } from "react-redux"; import setBgAction from "actions/setBgAction"; import setColorAction from "actions/setColorAction"; import Button from "components/CustomButton/CustomButton.jsx"; class FixedPlugin extends Component { constructor(props) { super(props); this.state = { classes: "dropdown show" }; this.handleClick = this.handleClick.bind(this); } handleClick() { if (this.state.classes === "dropdown") { this.setState({ classes: "dropdown show" }); } else { this.setState({ classes: "dropdown" }); } } render() { return ( 
  • SIDEBAR BACKGROUND
  • { this.props.setBgAction("black"); }} /> { this.props.setBgAction("white"); }} />
  • SIDEBAR ACTIVE COLOR
  • { this.props.setColorAction("primary"); }} /> { this.props.setColorAction("info"); }} /> { this.props.setColorAction("success"); }} /> { this.props.setColorAction("warning"); }} /> { this.props.setColorAction("danger"); }} />
  • Download now
  • Documentation
  • Want more components?
  • Get pro version
); } } const mapStateToProps = state => ({ ...state }); const mapDispatchToProps = dispatch => ({ setBgAction: (payload) => dispatch(setBgAction(payload)), setColorAction: (payload) => dispatch(setColorAction(payload)) }); export default connect(mapStateToProps, mapDispatchToProps)(FixedPlugin);

And we are done, you can start the project and see how everything works fine:

Original text


Multiple reducers

As you can have multiple actions, you can have multiple reducers. The only thing is that you need to combine them — we’ll see this a bit further down.

Let’s go ahead and create two new reducers for our app, one for the setBgAction and one for the setColorAction:

1 — Linux / Mac commands

touch src/reducers/bgReducer.js touch src/reducers/colorReducer.js

2 — Windows commands

echo "" > src\reducers\bgReducer.js echo "" > src\reducers\colorReducer.js

After this, let’s create the reducers’ code as follows:

src/reducers/bgReducer.js

export default (state = {}, action) => { switch (action.type) { case "bgChange": return { ...state, bgColor: action.payload }; default: return state; } };

src/reducers/colorReducer.js

export default (state = {} , action) => { switch (action.type) { case "colorChange": return { ...state, activeColor: action.payload }; default: return state; } };

When working with combined reducers, you need to add a default state in each of your reducers that are going to be combined. In my case, I’ve chosen an empty object i.e. state = {};

And now, our rootReducer will combine these two as follows:

src/reducers/rootReducer.js

import { combineReducers } from 'redux'; import bgReducer from 'reducers/bgReducer'; import colorReducer from 'reducers/colorReducer'; export default combineReducers({ activeState: colorReducer, bgState: bgReducer });

So, we say that we want the colorReducer to be referred by the activeState prop of the app state, and the bgReducer to be referred by the bgState prop of the app state.

This means that our state will no longer look like this:

state = { activeColor: "color1", bgColor: "color2" }

It will now look like this:

state = { activeState: { activeColor: "color1" }, bgState: { bgColor: "color2" } }

Since we’ve changed our reducers, now we’ve now combined them together into just one, we need to change our store.js as well:

src/store.js

import { createStore } from "redux"; import rootReducer from "reducers/rootReducer"; // we need to pass the initial state with the new look function configureStore(state = { bgState: {bgColor: "black"}, activeState: {activeColor: "info"} }) { return createStore(rootReducer,state); } export default configureStore;

Since we’ve changed the way the state looks, we now need to change the props inside the Sidebar and FixedPlugin components to the new state object:

src/components/Sidebar/Sidebar.jsx:

Change line 36 from

to

src/components/FixedPlugin/FixedPlugin.jsx:

We need to change all the this.props.bgColor to this.props.bgState.bgColor . And all the this.props.activeColor to this.props.activeState.activeColor .

So the new code should look like this:

import React, { Component } from "react"; import Button from "components/CustomButton/CustomButton.jsx"; import { connect } from "react-redux"; import setBgAction from "actions/setBgAction"; import setColorAction from "actions/setColorAction"; class FixedPlugin extends Component { constructor(props) { super(props); this.state = { classes: "dropdown show" }; this.handleClick = this.handleClick.bind(this); } handleClick() { if (this.state.classes === "dropdown") { this.setState({ classes: "dropdown show" }); } else { this.setState({ classes: "dropdown" }); } } render() { return ( 
  • SIDEBAR BACKGROUND
  • { this.props.setBgAction("black"); }} /> { this.props.setBgAction("white"); }} />
  • SIDEBAR ACTIVE COLOR
  • { this.props.setColorAction("primary"); }} /> { this.props.setColorAction("info"); }} /> { this.props.setColorAction("success"); }} /> { this.props.setColorAction("warning"); }} /> { this.props.setColorAction("danger"); }} />
  • Download now
  • Documentation
  • Want more components?
  • Get pro version
); } } const mapStateToProps = state => ({ ...state }); const mapDispatchToProps = dispatch => ({ setBgAction: (payload) => dispatch(setBgAction(payload)), setColorAction: (payload) => dispatch(setColorAction(payload)) }); export default connect(mapStateToProps, mapDispatchToProps)(FixedPlugin);

Let’s open the project again with npm start and see how everything works. Ta da!

Thanks for reading!

If you’ve enjoyed reading this tutorial please share it. I am very keen on hearing your thoughts about it. Just give this thread a comment and I’ll be more than happy to reply.

Special thanks should also go to Esther Falayi for his tutorial which has given me some much needed understanding on Redux.

Useful links:

  • Get the code for this tutorial from Github
  • Read more about ReactJS on their official website
  • Read more about Redux here
  • Read more about React-Redux
  • Check out our platform to see what we are doing and who we are
  • Get Paper Dashboard React from www.creative-tim.com or from Github
  • Read more about Reactstrap, the core of Paper Dashboard React

Find me on:

  • Email: [email protected]
  • Facebook: //www.facebook.com/NazareEmanuel
  • Instagram: //www.instagram.com/manu.nazare/
  • Linkedin: //www.linkedin.com/in/nazare-emanuel-ioan-4298b5149/