[ReactJS, Разработка веб-сайтов] Улучшаем useReducer
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
С появлением 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
===========
Похожие новости:
- [Разработка веб-сайтов] Иерархия компьютерных информационных систем для разработки сайта
- [JavaScript, Разработка веб-сайтов] Чем «фрагменты» могут помочь в Веб-разработке на примере Malina.js (перевод)
- [Laravel, PHP, Symfony, Разработка веб-сайтов] PhpStorm 2020.2: объединенные типы PHP 8, новый движок потока управления, пул-реквесты GitHub, OpenAPI
- [JavaScript, ReactJS, Программирование, Разработка веб-сайтов] О роли фронтенд-разработчика (перевод)
- [Бизнес-модели, Офисы IT-компаний, Разработка веб-сайтов, Финансы в IT] Как рассчитать стоимость работы компании?
- [Разработка веб-сайтов] День и ночь в интернете, или открытое письмо веб-разработчикам
- [JavaScript, Разработка веб-сайтов] Простое объяснение делегирования событий (перевод)
- [JavaScript, Node.JS, Разработка веб-сайтов] Lock-файлы npm
- [JavaScript, ReactJS, Тестирование веб-сервисов] Модульное и интеграционное тестирование в Redux Saga на примерах
- [Разработка веб-сайтов, Python, Программирование, Функциональное программирование] Какая асинхронность должна была бы быть в Python
Теги для поиска: #_reactjs, #_razrabotka_vebsajtov (Разработка веб-сайтов), #_react, #_redux, #_usereducer, #_reacthooks, #_reactjs, #_razrabotka_vebsajtov (
Разработка веб-сайтов
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 03:08
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
С появлением useReducer и useContext управление app state стало намного удобнее, а также отпала необходимость в использовании Redux. Когда я в первый раз отказался от Redux в пользу стандартного 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> ); } Выглядит приятнее, читаемость кода однозначно улучшилась. Какие улучшения мы получили:
Теперь давайте сравним как выглядит оптимизация 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 =========== Похожие новости:
Разработка веб-сайтов ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 03:08
Часовой пояс: UTC + 5