banner
他山之石

他山之石

Developing a simple To-do application with React-Redux

Redux focuses on state management and decouples from React. To facilitate usage, the author of Redux has encapsulated a React-specific library called React-Redux.

image

Previously, we learned the basic principles and usage of Redux through a simple counter application. In this article, we will introduce how Redux can be combined with React. To facilitate usage, the author of Redux has encapsulated a React-specific library called React-Redux, which will be the focus of the following sections.

For the complete code of this article, please check GitHub: https://github.com/YanYuanFE/redux-app

// clone repo
git clone https://github.com/YanYuanFE/redux-app.git

cd redux-app

// checkout branch
git checkout part-2

// install
npm install

// start
npm start

Installation#

npm install react-redux --save

Basic API#

Component#

React-Redux provides the Provider component, which wraps around the root component to pass the store to the internal components, allowing any descendant component within the application to easily access the global state. The core principle is based on the context API provided by React. The example code is as follows:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import { createStore } from 'redux';
import reducer from './reducers';

const store = createStore(reducer);

ReactDOM.render(
  <Provider store={store}>
    <App/>
  </Provider>,
  document.getElementById('root')
);

connect()#

React-Redux provides the connect method to connect React components with the Redux store. Through the connect method, we can pass the global state and Action Creators from the store into the props of any React component for use.

The connect method accepts two parameters, mapStateToProps and mapDispatchToProps, which define the input and output of the component. mapStateToProps is responsible for input logic, mapping the state to the component's props, while the latter is responsible for output logic, passing Action Creator methods into the component's props, allowing actions to be dispatched within the component. The example code is as follows:

import { connect } from 'react-redux';

const FilterLink = connect(
  mapStateToProps,
  mapDispatchToProps
)(Link);

export default FilterLink;

mapStateToProps#

mapStateToProps is a function that accepts two parameters, state and props. The state contains the entire global object tree, while props represent the component's props. Generally, the state is used most frequently. It returns an object, where the key-value pairs represent a mapping. The example code is as follows:

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

In the above code, within mapStateToProps, we calculate an object containing the key active based on the global state and the component's props, which can be accessed in the component through this.props.active.

mapDispatchToProps#

mapDispatch is a function or object used to map store.dispatch to the component's props. When mapDispatchToProps is a function, it accepts dispatch and ownProps as parameters and returns an object. The key-value pairs in the object define the methods passed to the component's props and the corresponding actions. The example code is as follows:

const mapDispatchToProps = (
  dispatch,
  ownProps
) => {
  return {
    onClick: () => {
      dispatch({
        type: 'SET_VISIBILITY_FILTER',
        filter: ownProps.filter
      });
    }
  };
}

The above code maps the dispatch method to the component's props parameter onClick, which can be called within the component using this.props.onClick().

When mapDispatch is an object, its key-value pairs are the component's props parameters and a function that acts as an Action Creator. In large applications, action creators are often defined in a dedicated file, as shown below:

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

The above code is one of a series of action creators that return an Action, which is automatically dispatched by Redux. The above code is used as follows:

import { setVisibilityFilter } from '../actions';

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

This method is essentially the same as writing it as a function; it just separates the action to avoid more redundant code, especially when the same action needs to be dispatched in multiple components.

Practical To-Do Application#

Having understood the core APIs of React-Redux, let's complete a full application.

The interface of the entire application is as follows:

image

The main functionalities include entering text to add to-do items, displaying the to-do list, toggling the status of to-do items by clicking on them, and filtering different to-do statuses by clicking on tabs.

During the application development process, we use components. First, we use create-react-app to initialize the React application. The src directory is the main development directory. Inside the src directory, we create actions, components, containers, and reducers folders. The actions folder is used to write action creators, the reducers folder is used to write the application's reducers, the components folder is used to write UI components, which are stateless components responsible only for UI presentation and do not handle business logic. Data is passed in through props, and they do not operate on Redux-related APIs. The containers folder is used to write container components, which manage data and business logic and operate on Redux APIs.

First, in App.js, we include the entire application's UI interface.

import React, { Component } from 'react';
import Footer from './components/Footer';
import AddTodo from './containers/AddTodo';
import VisibleTodoList from './containers/VisibleTodoList';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {

    return (
      <div className="App">
        <div className="todoapp">
          <AddTodo/>
          <VisibleTodoList/>
          <Footer/>
        </div>
      </div>
    );
  }
}

export default App;

As we can see, the entire application consists of three components: AddTodo, VisibleTodoList, and Footer.

Next, we will gradually introduce the implementation of the entire interface. Starting from the containers folder, the first is the AddTodo component, which is used to add to-dos.

import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../actions'

let AddTodo = ({ dispatch }) => {
  let input

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

export default AddTodo

AddTodo contains a form. In the onSubmit event of the form, it retrieves the input value and submits it to the action. The entire component is wrapped by Redux's connect component, allowing dispatch to be passed into the component's props.

Next is the addTodo method in the actions.

let nextTodoId = 0
export const addTodo = (text) => ({
  type: 'ADD_TODO',
  id: nextTodoId++,
  text
})

addTodo is used to create a new to-do, incrementing the to-do's id and passing in the text property of the to-do.

Next is the VisibleTodoList component, which displays the to-do list based on the currently selected tab.

import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'

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

const mapStateToProps = (state) => ({
  todos: getVisibleTodos(state.todos, state.visibilityFilter)
})

const mapDispatchToProps = {
  onTodoClick: toggleTodo
}

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

export default VisibleTodoList

From the code, we can see that VisibleTodoList does not contain any UI-related parts; it simply wraps the TodoList component and returns a new component. In mapStateToProps, based on all the todos and the filtering condition visibilityFilter, it returns a filtered list of todos through the getVisibleTodos function. mapDispatchToProps passes the action creator method into the UI component through props.

In the actions folder, the toggleTodo is defined as follows in index.js:

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

toggleTodo processes the current to-do being operated on by passing in the id.

Next is the TodoList component under components, which is responsible for displaying the to-do list.

import React from 'react'
import PropTypes from 'prop-types'
import Todo from './Todo'

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

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

export default TodoList

From the above code, we can see that TodoList receives the todos and onTodoClick from props and renders the todos. Each to-do is separated into a single Todo component, where the click event is passed to each Todo. In the click event, onTodoClick is called with the current to-do's id to change the to-do's status.

The Todo component is as follows:

import React from 'react'
import PropTypes from 'prop-types'

const Todo = ({ onClick, completed, text }) => (
  <li
    onClick={onClick}
    style={{
      textDecoration: completed ? 'line-through' : 'none'
    }}
  >
    {text}
  </li>
)

Todo.propTypes = {
  onClick: PropTypes.func.isRequired,
  completed: PropTypes.bool.isRequired,
  text: PropTypes.string.isRequired
}

export default Todo

In the TodoList component, when rendering all the to-do lists, each to-do is destructured and passed into the Todo component's props. In the Todo component, the required parameters onClick, completed, and text are retrieved. onClick is used to bind to the click event of the to-do, completed indicates whether the to-do's status is completed, and the style of the to-do is rendered based on the value of completed, while text is rendered as the content of the to-do.

Next is the Footer component in components:

import React from 'react'
import FilterLink from '../containers/FilterLink'

const Footer = () => (
  <p>
    Show:
    {" "}
    <FilterLink filter="SHOW_ALL">
      All
    </FilterLink>
    {", "}
    <FilterLink filter="SHOW_ACTIVE">
      Active
    </FilterLink>
    {", "}
    <FilterLink filter="SHOW_COMPLETED">
      Completed
    </FilterLink>
  </p>
)

export default Footer

The Footer component is used to filter the statuses of todos and contains three filtering options: SHOW_ALL to show all todos, SHOW_ACTIVE to show just added todos, and SHOW_COMPLETED to show completed todos. Each filtering option is separated into the FilterLink component, passing the filter as a filtering parameter.

Next is the code for FilterLink in the containers:

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

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

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

const FilterLink = connect(
  mapStateToProps,
  mapDispatchToProps
)(Link)

export default FilterLink

The FilterLink component is also responsible only for data logic and does not present any UI. In mapStateToProps, it calculates the active property passed to props by comparing the current component's props.filter with the filtering condition state.visibilityFilter. mapDispatchToProps defines the onClick method to dispatch an action to change the current filtering condition, passing the current component's filter property as a parameter.

The code for setVisibilityFilter in actions is as follows:

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

Next is the Link component.

import React from 'react'
import PropTypes from 'prop-types'

const Link = ({ active, children, onClick }) => {
  if (active) {
    return <span>{children}</span>
  }

  return (
    // eslint-disable-next-line
    <a href="#"
       onClick={e => {
         e.preventDefault()
         onClick()
       }}
    >
      {children}
    </a>
  )
}

Link.propTypes = {
  active: PropTypes.bool.isRequired,
  children: PropTypes.node.isRequired,
  onClick: PropTypes.func.isRequired
}

export default Link

The Link component receives active, children, and onClick properties through props. When active is true, the Link renders as a span; otherwise, it renders as an a tag. The a tag binds a click event, which triggers the onClick method in props when clicked.

Thus, all the code for the interface has been introduced. Next is the writing of reducers. In writing reducers, to clarify the responsibilities of reducers, we split them into todos and visibilityFilter. In todos.js, it handles adding todos and changing their statuses. The code is as follows:

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:
      return state
  }
}

export default todos

Here, todos is initialized as an empty array. The reducer must be a pure function that handles actions and cannot directly modify the state. It should return a new state each time. When adding a todo, the array spread operator is used to destructure the state, and the new todo object is added as the last value of the array, ultimately generating a completely new state. When modifying the todo status, the map method of the array is used to return a new state. Inside the map method, a ternary operator is used to compare the action parameter id with the id of each item in the array to find the currently clicked todo and toggle its completed value; otherwise, it returns the current todo.

In visibilityFilter.js, it is mainly used to change the filtering condition of the todo list. The code is as follows:

const visibilityFilter = (state = 'SHOW_ALL', action) => {
  switch (action.type) {
    case 'SET_VISIBILITY_FILTER':
      return action.filter
    default:
      return state
  }
}

export default visibilityFilter

At this point, the state is initialized to SHOW_ALL, meaning that when the application loads, it defaults to showing all todos. When handling actions, it directly returns action.filter.

When the reducers are split into individual files, the function names defined with const become the state values in the global state tree. We also need to combine the split reducers using Redux's combineReducers API. The following is the code for index.js in the reducers folder:

import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'

const todoApp = combineReducers({
  todos,
  visibilityFilter
})

export default todoApp

The final step is to wrap the entire application with the Provider API from React-Redux in index.js under src:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'
import './index.css';
import App from './App';
import { createStore } from 'redux';
import reducer from './reducers';

const store = createStore(reducer);

ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('root')
);

The browser's running effect is as follows:

image

Thus, the entire To-Do application has been developed. Compared to using Redux alone, React-Redux makes it easier for us to develop React applications.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.