欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

How to learn Redux from a functional programming perspective

最编程 2024-04-29 13:39:09
...

How to learn Redux from a functional programming perspective

Cristi SalcescuBlockedUnblockFollowFollowing
Photo by Fischer Twins on Unsplash

Redux is a state container that promotes the use of functional programming for managing state.

I would say that the Redux ecosystem has evolved in an architectural pattern that gives best practices on how to organize an application.

Pure Functions

Pure functions produce the same output value, given the same input. Pure functions have no side-effects.

Pure functions don’t mutate data, so the question is how can we change state and at the same time use pure functions. Redux proposes a solution: we write pure functions and let the library apply them and do the state change.

The application does state change, but the mutation is encapsulated behind the Redux store.

Immutability

An immutable value is a value that, once created, cannot be changed.

The state value is immutable, so each time we want to change the state we need to create a new immutable value.

The value of state is immutable but state can change. There is no point to use a library to manage state that doesn’t change. We can use a plain object to store that kind of data.

Architecture

Redux suggests that we split a practical application into the following parts:

  • Presentation Components
  • Action Creators (aka Synchronous Action Creators)
  • Reducers
  • Asynchronous Action Creators
  • API Utils/Gateways
  • Selectors
  • Container Components

Data Flow

Let’s look at the data flow for writes and reads. The data flow differs between synchronous and asynchronous write actions.

Writes to store

Writes with asynchronous actions

Some examples of asynchronous tasks are: network calls, timers calls.

Reads

A to-do application

In order to better understand all these parts, I created a to-do application.

The application takes all the to-dos from a REST API and displays them. The user can delete a to-do.

Here is how the UI looks like:

You can check the To-do application on codesandbox.io.

Presentation Components

I will start by defining the component that renders the list of to-dos.

The aim is to create components as pure functions.

TodoList renders the list of to-dos. TodoList is a pure function. It gets the to-dos and the onRemoveClick handler function and builds the JSX that describes how the UI looks like.

import React from "react";import TodoItem from "./TodoItem.jsx";
export default function TodoList({ todos, onRemoveClick }) {  function renderTodoItem(todo) {     return <TodoItem todo={todo} key={todo.id}               onRemoveClick={onRemoveClick} />;  }
return <ul className="todo-list">{todos.map(renderTodoItem)}</ul>;}

TodoListItem renders a single to-do. It is a pure function.

import React from "react";import partial from "lodash/partial";
export default function TodoItem({ todo, onRemoveClick }) {  const triggerRemoveClick = partial(onRemoveClick, todo);
return (    <li>      <div>{todo.title}</div>      <div>{todo.userName}</div>      <div>        <button onClick={triggerRemoveClick}>Delete</button>      </div>    </li>  );}

Both TodoList and TodoListItem are presentation components.

The presentation components communicate only through their own properties.

Actions

Actions are plain serializable objects containing all the necessary information to make that action.

For example, what are the necessary information to remove a to-do? First the application needs to know that it is a “remove to-do” action, and second it needs to know the to-do object to be removed. Here is how the “remove to-do” action may look like:

{  type: "REMOVE_TODO",  todo}

The action object needs the type property that indicates the action to perform. type is usually a string.

As action objects move around the application, I suggest to make them immutable.

Action Creators

In order to create these plain action objects, the practice is to encapsulate the code that creates them in functions. These functions are pure functions called action creators.

function resetTodos(todos) {  return Object.freeze({    type: "RESET_TODOS",    todos  });}
function removeTodo(todo) {  return Object.freeze({    type: "REMOVE_TODO",    todo  });}
export { resetTodos, removeTodo };

Store

The store manages state.

In Redux there is a single store that manages all the application state tree.

It has the getState() method that gets all state as a read-only immutable value.

Dispatcher

The store in Redux is a dispatcher. For this, it has a dispatch() method.

The state inside the store can be changed only by dispatching actions. There are no state setters on the store object.

Reducers

In Redux, the store is created using pure functions. These functions take the state and an action as parameters and return the new state.

We define the pure functions and let the store apply them when an action is dispatched. These functions are called reducers.

Let’s write the reducer used to change the todos.

import matchesProperty from "lodash/matchesProperty";
export default function todos(todos = [], action) {  switch (action.type) {    case "REMOVE_TODO":      const index = todos.findIndex(matchesProperty("id", action.todo.id));      return [...todos.slice(0, index), ...todos.slice(index + 1)];    case "RESET_TODOS":      return action.todos;    default:      return todos;  }}

When the "REMOVE_TODO" action is dispatched, the state is changed to a new list that doesn’t contain the deleted to-do. The deleted to-do is found in action.todo.

When the "RESET_TODOS" action is dispatched, the state is changed to a new list with all the new to-dos. The new to-dos are taken from action.todos.

Reducers are used to change state, not to get the state from the store.

Root Reducer

Redux requires one root reducer. We can create many reducers managing parts of the root state. Then combine them together with combineReducers() and create the root reducer.

import { combineReducers } from "redux";import todos from "./todos";
export default combineReducers({  todos});

Here is how the whole flow inside the store looks like:

API Utils/Gateways

The application may do network calls. We can encapsulate these calls in their own files.

Functions doing network calls are impure.

const url = "https://jsonplaceholder.typicode.com/todos";
function toJson(response) {  return response.json();}
function fetchTodos() {  return fetch(url).then(toJson);}
function remove(todo) {  const itemUrl = `${url}/${todo.id}`;  return fetch(itemUrl, {    method: "DELETE"  }).then(toJson);}
export { fetchTodos, remove };

Asynchronous Action Creators

The action creators we have already defined are only for modifying the state in the store, nothing else.

An application may do different tasks: network calls, timer calls, state change, etc. We need a way to coordinate all these tasks.

In our case, we need to make the network call take the to-dos, and then update the store by dispatching an action with the new to-dos.

In order to delete a to-do, we need to make a network call to delete the to-do on the server, and then dispatch an action to remove the to-do from the store.

We can do this orchestration logic using the middleware asynchronous action creators.

import * as Gateway from "../api/TodoGateway";import * as StoreActions from "../actions/TodoStoreActions";
function fetchAndResetTodos() {  return function(dispatch) {    return Gateway.fetchTodos().then(data =>      dispatch(StoreActions.resetTodos(data))    );  };}
function fetchRemoveTodo(todo) {  return function(dispatch) {    return Gateway.remove(todo).then(data =>      dispatch(StoreActions.removeTodo(todo))    );  };}
export { fetchAndResetTodos, fetchRemoveTodo };

Asynchronous action creators are functions that return other functions. The returned functions are called “thunks”.

We can enable them with redux-thunks .

Entry Point

The main.js is the application a single entry point.

Here is where the store is created.

We can send the store down the components’ tree using props, but a simpler way is to use the React Context.

The Provider component from the react-redux package can be used to send the store down the components’ tree.

import React from "react";import { render } from "react-dom";import { createStore, applyMiddleware } from "redux";import { Provider } from "react-redux";import rootReducer from "./store";import thunk from "redux-thunk";
import "./styles.css";import App from "./App.jsx";import { fetchAndResetTodos } from "./middleware/TodoMiddlewareActions";
const store = createStore(rootReducer, applyMiddleware(thunk));
store.dispatch(fetchAndResetTodos());
render(  <Provider store={store}>    <App />  </Provider>,  document.getElementById("root"));

Selectors

There are cases when we want to filter and transform the state. For this, we use pure functions that take state as the first parameter and return the transformed result. I will call them selector functions and put them in the same file as the reducer function that works with the same state.

Below you can see the getBy() selector that takes the list of to-dos and returns a new sorted list containing only the top items.

function byIdDesc(todo1, todo2) {  return todo2.id - todo1.id;}
function getBy(todos) {  const top = 25;  return todos.sort(byIdDesc).slice(0, top);}
export { getBy };

Container Component

TodoList is a presentation component that requires the list of todos in order to render the UI.

We need a way to take the data from the store, process it and send it to the TodoList component. This is the role of the container component.

For this, I will use the connect() higher order component from redux-connect package.

The TodoListContainer component will do the following:

  • take the state from store, process it with a selector and then send the result to the presentation component as props. The mapStateToProps() does that.
  • define what action will be dispatched on each user interaction. The mapDispatchToProps() does that.
import { connect } from "react-redux";import { flowRight } from "lodash";import TodoList from "./TodoList.jsx";import { fetchRemoveTodo } from "../middleware/TodoMiddlewareActions";import { getBy } from "../store/todos";
function mapStateToProps(state) {  return {    todos: getBy(state.todos)  };}
function mapDispatchToProps(dispatch) {  return {    onRemoveClick: flowRight([dispatch, fetchRemoveTodo])  };}
export default connect(  mapStateToProps,  mapDispatchToProps)(TodoList);

Packages

Here are the packages needed to install Redux and the other related libraries used in this application.

{  "dependencies": {    "redux": "^3.5.2",    "react-redux": "^5.0.7",    "redux-thunk": "^2.3.0"  }}

Conclusion

Pure functions are easier to reason about.

Redux is a state manager working with pure functions.

A practical Redux application implies to split the code in: presentation and container components, reducers and selectors, synchronous and asynchronous action creators.

By following the Redux practices, I managed to create more pure functions inside the application. Presentation components, action creators, reducers, selectors are all pure functions.

You can check the To-do application on codesandbox.io.

For more on JavaScript take a look at:

Discover Functional Programming in JavaScript with this thorough introduction

Let’s experiment with functional generators and the pipeline operator in JavaScript

Learn these JavaScript fundamentals and become a better developer

Let’s explore objects in JavaScript

How point-free composition will make you a better functional programmer

How to make your code better with intention-revealing function names

Make your code easier to read with Functional Programming