In this section, we’ll introduce the concepts of container components, which take over the state, and display components, which render the interface. The Display component was also the heart of React and focuses on writing an efficient user interface.

Refactoring code: Separate TodoList state from rendering

Welcome to the Redux Package series:

  • Redux Redux Redux Redux Redux Redux Redux Redux Redux Redux
  • Redux: Strike while the iron is hot and completely refactor
  • Redux Education Package (3) : Each To perform their duties, to regain the original purpose (namely this article)

This tutorial is part of the React Front Engineer tutorial. Click here to see it all.

Show components and container components

You’d think Redux would be good enough to keep State sources predictable by stripping them from the React component and storing them in the Store. But Redux isn’t done yet. It further introduces the concepts of Presentational Components and Container Components, separating the purely Presentational React component from state.

When we break down the View layer in the Redux state loop diagram further, it looks like this:

That is, we add a layer between the final rendering component and the State stored in the Store. We call this layer responsible for receiving State from the Store and assembling the State changes that the component wants to initiate into actions, which are then issued through the Dispatch function.

The layer that remains after the state has been stripped completely is called the presentation component, which takes the data from the container component, renders it into a UI, and tells the container component to dispatch actions on its behalf when the state needs to change.

First, we move VisibilityFilters from app.js to SRC/Actions /index.js. Because VisibilityFilters define three operations that filter and display TodoList that are a little more similar to actions, we put together similar things. Change SRC /actions/index.js as follows:

let nextTodoId = 0;

export const addTodo = text= > ({
  type: "ADD_TODO".id: nextTodoId++,
  text
});

export const toggleTodo = id= > ({
  type: "TOGGLE_TODO",
  id
});

export const setVisibilityFilter = filter= > ({
  type: "SET_VISIBILITY_FILTER",
  filter
});

export const VisibilityFilters = {
  SHOW_ALL: "SHOW_ALL".SHOW_COMPLETED: "SHOW_COMPLETED".SHOW_ACTIVE: "SHOW_ACTIVE"
};
Copy the code

Writing container components

The Container component is also a React component, which simply removes the store-to-view state and dispatch Action logic from the component.

According to Redux best practices, container components are generally stored in containers folders. We create a Containers folder under SRC and create a new visibleTodolist.js file inside. To represent the original todolist.js container component and add the following code to the file:

import { connect } from "react-redux";
import { toggleTodo } from ".. /actions";
import TodoList from ".. /components/TodoList";
import { VisibilityFilters } from ".. /actions";

const getVisibleTodos = (todos, filter) = > {
  switch (filter) {
    case VisibilityFilters.SHOW_ALL:
      return todos;
    case VisibilityFilters.SHOW_COMPLETED:
      return todos.filter(t= > t.completed);
    case VisibilityFilters.SHOW_ACTIVE:
      return todos.filter(t= >! t.completed);default:
      throw new Error("Unknown filter: "+ filter); }};const mapStateToProps = state= > ({
  todos: getVisibleTodos(state.todos, state.filter)
});

const mapDispatchToProps = dispatch= > ({
  toggleTodo: id= > dispatch(toggleTodo(id))
});

export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
Copy the code

As you can see, the above code mainly does these things:

  • We defined onemapStateToPropsThis function is used to obtain the State from the Redux Store and the original Props of the component itself, combine the two to form new Props, and pass it to the component. This function is the only interface from the Store to the component. Here we define the previous asApp.jsIn thegetVisibleTodosThe delta function moves over, and the delta function moves overstate.filterFilter criteria return the corresponding displaytodos.
  • And then we define one that we haven’t seen beforemapDispatchToPropsFunction, which takes two arguments:dispatchownPropsThe former is the familiar function that issues the update action, and the latter is the Props of the original component, which is an optional parameter that we don’t declare here. We mainly define all the requirements in this function declarationdispatchAction function, and pass it to the component as Props. So here we’ve defined onetoggleTodoFunction that makes the call through the componenttoggleTodo(id)You candispatch(toggleTodo(id))
  • Finally we go through the familiarconnectThe function to receivemapStateToPropsmapDispatchToPropsAnd call the TodoList component, which then receives and calls the TodoList component, returning the final exported container component.

Writing presentation components

Once we have written the TodoList container component, the next thing we need to consider is pulling out the TodoList presentation component from State and Dispatch.

Open the SRC/components/TodoList. Js make corresponding changes to the file is as follows:

import React from "react";
import PropTypes from "prop-types";
import Todo from "./Todo";

const TodoList = ({ todos, toggleTodo }) = > (
  <ul>
    {todos.map(todo => (
      <Todo key={todo.id} {. todo} onClick={()= > toggleTodo(todo.id)} />
    ))}
  </ul>
);

TodoList.propTypes = {
  todos: PropTypes.arrayOf(
    PropTypes.shape({
      id: PropTypes.number.isRequired,
      completed: PropTypes.bool.isRequired,
      text: PropTypes.string.isRequired
    }).isRequired
  ).isRequired,
  toggleTodo: PropTypes.func.isRequired
};

export default TodoList;
Copy the code

In the code above, we remove the Connect and toggleTodo actions and remove the dispatch attribute received by TodoList and replace it with the toggleTodo function passed in via mapDispatchToProps. And the toggleTodo function is called when Todo is clicked.

Of course our toggleTodo property is back, so we restore the toggleTodo we removed earlier in propTypes. 🙂

Finally, we no longer need connect()(TodoList) because the corresponding container component of TodoList defined in VisibleTodolist.js takes the State in the Redux Store and passes it to TodoList.

As you can see, TodoList doesn’t have to worry about state-related operations any more. It just needs to concentrate on displaying the interface and responding to actions. We further separated state from rendering, letting the right people do what they do best.

A few loose ends

Because we have stripped the original TodoList into container components and presentation components, we need to change the corresponding TodoList in app.js into our VisibleTodoList, and the container component provides the external interface of the original TodoList.

We open SRC /components/ app.js and make the following changes:

import React from "react";
import AddTodo from "./AddTodo";
import VisibleTodoList from ".. /containers/VisibleTodoList";
import Footer from "./Footer";

import { connect } from "react-redux";

class App extends React.Component {
  render() {
    const { filter } = this.props;

    return (
      <div>
        <AddTodo />
        <VisibleTodoList />
        <Footer filter={filter} />
      </div>
    );
  }
}

const mapStateToProps = (state, props) => ({
  filter: state.filter
});

export default connect(mapStateToProps)(App);
Copy the code

You can see that we did a couple of things:

  • Replace the previous TodoList with VisibleTodoList.
  • Delete VisibilityFilters because it’s already in theresrc/actions/index.js
  • Delete getVisibleTodos as it is already placed in VisibleTodoList.
  • deletemapStateToPropsTo derivetodosBecause we already got it in VisibleTodoList.
  • Delete the corresponding App componenttodos.

Next, let’s deal with the packet guide of several other files caused by changes in VisibilityFilters.

Open SRC /components/ footer.js to change the package path:

import React from "react";
import Link from "./Link";
import { VisibilityFilters } from ".. /actions";

import { connect } from "react-redux";
import { setVisibilityFilter } from ".. /actions";

const Footer = ({ filter, dispatch }) = > (
  <div>
    <span>Show: </span>
    <Link
      active={VisibilityFilters.SHOW_ALL= = =filter}
      onClick={()= > dispatch(setVisibilityFilter(VisibilityFilters.SHOW_ALL))}
    >
      All
    </Link>
    <Link
      active={VisibilityFilters.SHOW_ACTIVE= = =filter}
      onClick={()= >
        dispatch(setVisibilityFilter(VisibilityFilters.SHOW_ACTIVE))
      }
    >
      Active
    </Link>
    <Link
      active={VisibilityFilters.SHOW_COMPLETED= = =filter}
      onClick={()= >
        dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))
      }
    >
      Completed
    </Link>
  </div>
);

export default connect()(Footer);
Copy the code

Open SRC /reducers/filter.js to modify the package guide path:

import { VisibilityFilters } from ".. /actions";

const filter = (state = VisibilityFilters.SHOW_ALL, action) = > {
  switch (action.type) {
    case "SET_VISIBILITY_FILTER":
      return action.filter;
    default:
      returnstate; }};export default filter;
Copy the code

Since the nextTodoId in SRC/Actions /index.js is increment from 0, there will be some problems with the initialTodoState we defined before. For example, the ID of the newly added TOdo will overlap with the initial one, leading to problems. Therefore, we deleted the corresponding initialTodoState in SRC /reducers/todos.js, and then gave the state of the todos Reducer a default value of [].

const todos = (state = [], action) = > {
  switch (action.type) {
    case "ADD_TODO": {
      return [
        ...state,
        {
          id: action.id,
          text: action.text,
          completed: false}]; }case "TOGGLE_TODO": {
      return state.map(todo= >
        todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
      );
    }

    default:
      returnstate; }};export default todos;
Copy the code

summary

Save the changes and you’ll see that our to-do applet still runs intact, but we’ve successfully separated the original TodoList into the Container component VisibleTodoList and the presentation component TodoList.

Refactoring code: Separate the state of the Footer from the render

Let’s strike while the iron is hot and use what we learned in the previous section to immediately pull the state and render out of the Footer component.

Writing container components

We create a filterlink.js file under the SRC /containers folder and add the corresponding contents as follows:

import { connect } from "react-redux";
import { setVisibilityFilter } from ".. /actions";
import Link from ".. /components/Link";

const mapStateToProps = (state, ownProps) = > ({
  active: ownProps.filter === state.filter
});

const mapDispatchToProps = (dispatch, ownProps) = > ({
  onClick: (a)= > dispatch(setVisibilityFilter(ownProps.filter))
});

export default connect(mapStateToProps, mapDispatchToProps)(Link);
Copy the code

As you can see, we did the following:

  • definemapStateToProps, which is responsible for comparing states stored in the Redux Storestate.filterProperties and components receive descends from the parentownProps.filterWhether the attributes are the same, if the same, thenactiveSet totrue.
  • definemapDispatchToProps, it does this by returning aonClickFunction that generates one when the component is clickeddispatchAction that receives the component from the parentownProps.filterParameter passed insetVisibilityFilterTo generate theaction.type"SET_VISIBILITY_FILTER"The Action, anddispatchThis Action.
  • And finally we go throughconnectCombine the two and merge the corresponding properties intoLinkComponent and export. We should be here right nowLinkComponent that we defined in the two functions aboveactiveonClickProperties.

Writing presentation components

SRC/Components/footer.js file and make the following changes:

import React from "react";
import FilterLink from ".. /containers/FilterLink";
import { VisibilityFilters } from ".. /actions";

const Footer = (a)= > (
  <div>
    <span>Show: </span>
    <FilterLink filter={VisibilityFilters.SHOW_ALL}>All</FilterLink>
    <FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>Active</FilterLink>
    <FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>Completed</FilterLink>
  </div>
);

export default Footer;
Copy the code

As you can see, the code changes above do several things:

  • So let’s export the previous oneLinkReplaced with aFilterLink. Note that when component state and rendering are separated, we will use container components for components exported for use by other components.
  • We use theFilterLinkComponent and pass the corresponding threeFilterLinkFilter type.
  • Then we delete what is no longer neededconnectsetVisibilityFilterThe export.
  • Finally delete what is no longer neededfilterdispatchProperties as they are already inFilterLinkIs defined and passed toLinkComponents.

Delete unnecessary content

SRC /components/ app.js: SRC /components/ app.js: SRC /components/ app.js: SRC /components/ app.js: SRC /components/ app.js

import React from "react";
import AddTodo from "./AddTodo";
import VisibleTodoList from ".. /containers/VisibleTodoList";
import Footer from "./Footer";

class App extends React.Component {
  render() {
    return (
      <div>
        <AddTodo />
        <VisibleTodoList />
        <Footer />
      </div>); }}export default App;
Copy the code

As you can see, we did the following:

  • deleteAppComponent corresponding tofilterProperties andmapStateToPropsDelta function, because we already have delta functionFilterLinkSo we no longer need to pass it directly from the App component to the Footer component.
  • Delete the correspondingconnectFunction.
  • Delete the correspondingconnect(mapStateToProps)()Because apps no longer need to get content directly from the Redux Store.

summary

Save the changes and you’ll see that our to-do widget still works, but we’ve successfully separated the original Footer into the container component’s FilterLink and the display component’s Footer.

Refactoring code: Separate AddTodo state from rendering

Let’s finish up by separating the state and rendering of the AddTodo component.

Writing container components

We create the addTodoContainer.js file in the SRC /containers folder and add the following to it:

import { connect } from "react-redux";
import { addTodo } from ".. /actions";
import AddTodo from ".. /components/AddTodo";

const mapStateToProps = (state, ownProps) = > {
  return ownProps;
};

const mapDispatchToProps = dispatch= > ({
  addTodo: text= > dispatch(addTodo(text))
});

export default connect(mapStateToProps, mapDispatchToProps)(AddTodo);
Copy the code

You can see that we did a few familiar things:

  • definemapStateToPropsBecause theAddTodoI don’t need to pull anything from the Redux Store, somapStateToPropsJust fill it inconnectAnd simply return the original of the componentprops, has no other effect.
  • definemapDispatchToPropsWe defined oneaddTodoFunction, it acceptstextAnd thendispatchaaction.type"ADD_TODO"The Action.
  • And finally we go throughconnectCombine the two and merge the corresponding properties intoAddTodoComponent and export. We should be here right nowAddTodoComponent that we defined in the two functions aboveaddTodoProperties.

Writing presentation components

Then we write AddTodo display component part, open the SRC/components/AddTodo js file, make the following change to the corresponding content:

import React from "react";

const AddTodo = ({ addTodo }) = > {
  let input;

  return (
    <div>
      <form
        onSubmit={e= >{ e.preventDefault(); if (! input.value.trim()) { return; } addTodo(input.value); input.value = ""; }} ><input ref={node= > (input = node)} />
        <button type="submit">Add Todo</button>
      </form>
    </div>
  );
};

export default AddTodo;
Copy the code

As you can see, the above code does a few things:

  • We deleted the exportedconnectFunction, and get rid of the pairAddTodoThe parcel.
  • We will beAddTodoReceive attributes fromdispatchReplace fromAddTodoContainerTo get theaddTodoFunction that will be called when the form is submitted,dispatchaaction.type"ADD_TODO".textinput.valueThe Action.

Modify the corresponding content

Since we split the original TodoList into the container component AddTodoContainer and the presentation component TodoList, we need to make the following changes to SRC/Components/app.js:

import React from "react";
import AddTodoContainer from ".. /containers/AddTodoContainer";
import VisibleTodoList from ".. /containers/VisibleTodoList";
import Footer from "./Footer";

class App extends React.Component {
  render() {
    return (
      <div>
        <AddTodoContainer />
        <VisibleTodoList />
        <Footer />
      </div>); }}export default App;
Copy the code

You can see that we replaced the AddTodo export with AddTodoContainer and rendered the AddTodoContainer component in the Render method.

summary

Save the changes and you’ll see that our to-do applet still runs intact, but we’ve successfully separated the original AddTodo into AddTodoContainer for the container component and AddTodo for the presentation component.

conclusion

So far, we’ve learned all the basic concepts of Redux and used them to refactor a react-only backlog into Redux step by step.

Let’s take a final look at the Redux state loop diagram and review what we’ve learned in this tutorial:

In this tutorial we first proposed the three concepts of Redux: Store, Action, Reducers:

  • Store is used to hold the State of the entire application, which is a JavaScript object called State. All app states are retrieved from the Store, so any state change is a change of state in the Store, hence the Store has the title of “the only source of truth for data”.
  • Action is the only means used in Redux to change the state of the Store, and all state changes are similar{ type: 'ACTION_TYPE', data1, data2 }Such a formal declaration defines an Action and then passesdispatchThis Action is going to happen.
  • Reducers are used in response to actions that emit changes through actionsswitchStatement matchaction.typeReturn a new State operation by adding, deleting, changing, or checking the properties of State. It is also a pure function, that is, it does not change directlyStateItself.

Specifically reflected in our reconfigured to-do items, we used Store state to replace this.state in React, and used Action to replace the Action of changing this.state in React. Initiate the operation of modifying the State in Store through the Dispatch Action, use Reducers to replace the this.setState operation of updating the State in React, and purify and update the State saved in Store.

We then struck while the iron was hot, using the three concepts we had learned, and refactoring the rest of the entire to-do list into Redux.

However, after refactoring, we found that our rootReducer function was already a bit bloated, containing todos and Filter status attributes, and adding different status attributes if we wanted to extend the to-do application. When the operation of various state attributes is mixed together, it is easy to cause confusion and reduce the readability of the code, which is not conducive to maintenance. Therefore, we propose combineReducers method, which is used to cut the rootReducer into a Reducer with a single state attribute scattered in different files. Then combine these split Reducers through combineReducers.

After explaining the concept of combineReducers in detail, we then reconstructed the previous incomplete Redux code again, and split rootReducer into two Reducer: Todos and filter.

Finally, we went one step further and asked React to focus on user interface writing and separate application state from rendering. We proposed the concept of display components and container components. The former is completely React, receives data from React, and is responsible for rendering data efficiently and correctly. Redux is responsible for responding to the user’s actions and then instructs the user to issue specific commands. When we use Redux, we add a layer of logic on React that takes care of the state.

I hope you can have a good understanding of Redux and use React and Redux flexibly. Thank you for reading!

One More Thing!

Careful readers may have noticed that the Redux state cycle graph we drew is one-way with a clear arrow pointing to it, which is actually the philosophy of Redux, namely “one-way data flow”, and the design mode recommended by the React community. Combined with the pure function convention of Reducer, This allows us to record and reproduce every state change throughout the application, or state is predictable and can be traced back to an Action that initiated a state change, hence Redux’s title as a “predictable state management container.”

This tutorial is part of the React Front Engineer tutorial. Click here to see it all.

Want to learn more exciting practical skills tutorial? Come and visit the Tooquine community.