[ReactJS, Разработка веб-сайтов] Улучшаем useReducer

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
03-Авг-2020 13:30

С появлением useReducer и useContext управление app state стало намного удобнее, а также отпала необходимость в использовании Redux.
Когда я в первый раз отказался от Redux в пользу стандартного useReducer я ощутил нехватку некоторых полезных функций:
  • useSelector. Позволяет оптимизировать перерисовку компонент, которые используют useContext, с помощью memo.
  • Единственный dispatch. Упрощает обновление app state так как не нужно использовать отдельный dispatch каждого useReducer.
  • Cache. Не нужно заботиться о кэширование каждого useReducer.

Тогда я решил попробовать улучшить стандартный useReducer, добавив эти 3 функции. Эта идея превратилась в новую небольшую библиотеку, которую я назвал Flex Reducer.
Интересный факт!
Flex Reducer не использует ни useReducer ни useContext в своей реализации.
Посмотрим плюсы и минусы использования стандартных useReducer + useContext и Flex Reducer на примере типичного Todo App.
Для начала создадим главный файл, где отрисовывается дерево React.
// index.js
import TodoApp from "./TodoApp";
const rootElement = document.getElementById("root");
ReactDOM.render(
  <TodoApp />,
  rootElement
);

Замечание: Больше никаких combine reducers, create store и Provider. Ура! :)
Теперь напишем основной компонент Todo App, используя useReducer.
// TodoApp.js
import { useReducer, createContext, useMemo } from 'react';
import AddTodo from './AddTodo';
import TodoList from './TodoList';
export const AppContext = createContext(null);
const cache = {};
export default function TodoApp() {
  const [state, dispatch] = useReducer(reducer, cache.state || initialState);
  cache.state = state;
  const actions = useMemo(() => ({
    setInput: (value) => {
      dispatch({
        type: 'SET_INPUT',
        payload: value
      })
    },
    addTodo: ({ id, content }) => {
      dispatch({
        type: 'ADD_TODO',
        payload: { id, content }
      })
    }
  }), []);
  return (
    <AppContext.Provider value=[state, actions]>
      <div className="todo-app">
        <h1>{state.title}</h1>
        <input value={state.input} onChange={e => actions.setInput(e.target.value)} />
        <AddTodo />
        <TodoList />
      </div>
    </AppContext>
  );
}

Выглядит неплохо. Теперь то же самое, но используя Flex Reducer.
// TodoApp.js
import { useFlexReducer, dispatch } from 'flex-reducer';
import AddTodo from './AddTodo';
import TodoList from './TodoList';
export const setInput = (value) => dispatch({
  type: SET_INPUT,
  payload: value
});
export const addTodo = ({ id, content }) => dispatch({
  type: ADD_TODO,
  payload: { id, content }
});
export default function TodoApp() {
  const [state] = useFlexReducer('app', reducer, initialState);
  return (
    <div className="todo-app">
      <h1>{state.title}</h1>
      <input value={state.input} onChange={e => setInput(e.target.value)} />
      <AddTodo />
      <TodoList />
    </div>
  );
}

Выглядит приятнее, читаемость кода однозначно улучшилась.
Какие улучшения мы получили:
  • Не нужно использовать React Context.
  • Не нужно заботиться о кэшировании.
  • Можем объявлять actions где угодно так как есть глобальный dispatch.

Теперь давайте сравним как выглядит оптимизация re-renders на примере Add Todo button.
С использованием стандартных хуков.
// AddTodo.js
import { useContext, memo } from 'react';
import { appContext } from './TodoApp';
const genId = () => Math.rand();
const AddTodo = memo(({ input, actions }) => {
  function handleAddTodo() {
    if (content) {
      actions.addTodo({ id: genId(), content: input });
      actions.setInput('');
    }
  }
  return (
    <button onClick={handleAddTodo}>
      Add Todo
    </button>
  );
})
export default const MemoizedAddTodo = () => {
  const [state, actions] = useContext(appContext);
  return (
    <AddTodo input={state.input} actions={actions} />
  );
}

Мы не можем использовать useContext в AddTodo напрямую потому что это будет вызывать render на каждое изменение контекста в независимости от использования memo. Поэтому нам придется создать еще один компонент-обертку, в который вынесем useContext и будем передавать нужные данные через props.
Теперь попробуем оптимизацию с Flex Reducer.
// AddTodo.js
import { useSelector } from 'flex-reducer';
import { addTodo, setInput } from "./TodoApp";
const genId = () => Math.rand();
export default const AddTodo = React.memo(() => {
  const content = useSelector(state => state.app.input);
  function handleAddTodo() {
    if (content) {
      addTodo({ id: genId(), content });
      setInput('');
    }
  }
  return (
    <button onClick={handleAddTodo}>
      Add Todo
    </button>
  );
})

Прекрасно. Не нужен никакой дополнительный компонент-обертка. Спасибо useSelector, который вызывает re-render только когда меняется input.
Однако, все в этом мире имеет свои плюсы и минусы, Flex Reducer не исключение.
Давайте сравним как он будет работать с remote data, которые загружаются декларативно, например с помощью react-query.
В случае со стандартным useReducer.
// TodoApp.js
import { useReducer, createContext, useMemo } from 'react';
import { useQuery } from 'react-query';
import AddTodo from './AddTodo';
import TodoList from './TodoList';
export const AppContext = createContext(null);
const cache = {};
export default function TodoApp() {
  const [reducerState, dispatch] = useReducer(reducer, cache.state || initialState);
  cache.state = reducerState;
  const actions = useMemo(() => ({
    setInput: (value) => {
      dispatch({
        type: 'SET_INPUT',
        payload: value
      })
    },
    addTodo: ({ id, content }) => {
      dispatch({
        type: 'ADD_TODO',
        payload: { id, content }
      })
    }
  }), []);
  const todos = useQuery('todos', fetchTodoList);
  const state = { ...reducerState, todos };
  return (
    <AppContext.Provider value=[state, actions]>
      <div className="todo-app">
        <h1>{state.title}</h1>
        <input value={state.input} onChange={e => actions.setInput(e.target.value)} />
        <AddTodo />
        <TodoList />
      </div>
    </AppContext>
  );
}

Неплохо. Просто и читаемо.
Попробуем то же самое с Flex Reducer.
// TodoApp.js
import { useFlexReducer, dispatch } from 'flex-reducer';
import { useQuery } from 'react-query';
import AddTodo from './AddTodo';
import TodoList from './TodoList';
export const setInput = (value) => dispatch({
  type: SET_INPUT,
  payload: value
});
export const addTodo = ({ id, content }) => dispatch({
  type: ADD_TODO,
  payload: { id, content }
});
export const setTodos = (todos) => dispatch({
  type: SET_TODOS,
  payload: todos
});
export default function TodoApp() {
  const [state] = useFlexReducer('app', reducer, initialState);
  const todos = useQuery('todos', fetchTodoList);
  React.useEffect(() => {
    setTodos(todos);
  }, [todos]);
  return (
    <div className="todo-app">
      <h1>{state.title}</h1>
      <input value={state.input} onChange={e => setInput(e.target.value)} />
      <AddTodo />
      <TodoList />
    </div>
  );
}

Возникает проблема с лишней перерисовкой когда обновляется состояние редьюсера на каждое обновление todos query.
Заключение
Использование useReducer + useContext для управления app state вполне удобно. Но это требует внимательной "ручной" работы с контекстом и кэшем.
Flex Reducer берет это на себя, улучшает читаемость кода, облегчает написание оптимизации с memo и сокращает количество когда. Но появляются проблемы при работе с remote data декларативно (как с react-query).
Внимание!
Flex Reducer всего лишь эксперимент и не использовался в production.
Спасибо, что читали. Приветствуются любые мысли.
Рабочий пример Todo app можно найти тут.
Ссылка на репозиторий Flex Reducer
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_reactjs, #_razrabotka_vebsajtov (Разработка веб-сайтов), #_react, #_redux, #_usereducer, #_reacthooks, #_reactjs, #_razrabotka_vebsajtov (
Разработка веб-сайтов
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 21:59
Часовой пояс: UTC + 5