React Junior
207 subscribers
37 photos
462 links
Изучение React с нуля
加入频道
Валидация форм в React-приложениях с помощью хука useForm

Статья (англ.): https://blog.openreplay.com/react-form-validation-with-the-useform-hook

Валидация форм в вебе - очень важная задача, которая так до сих пор и не стандартизирована полноценно. Существуют десятки разных решений, и в статье разбирается одно из них - библиотека React Hook Form https://react-hook-form.com/get-started.

Эта библиотека небольшая, производительная и довольно популярная, как понятно из названия она использует хуки, которые де-факто являются сейчас стандартом в React разработке.

Предлагаемый библиотекой api интуитивно понятный, хоть и немного громоздкий (впрочем, где вы видели негромоздкую валидацию, да?)

https://codesandbox.io/s/react-hook-form-react-junior-qyv1if?file=/src/App.js

Все базовые потребности есть: установка правил валидации, вывод ошибок, отслеживание изменений, отслеживание фокуса и т.д.

Использование

Хук useForm может принимать базовые настройки валидации (можно ничего не передавать и использовать дефолтные) и возвращает несколько методов (`register`, handleSubmit`), а также объект `formState. По названиям очевидно, для чего они предназначены:

* register - регистрирует каждое валидируемое поле с конкретными настройками валидации.
* в handleSubmit нужно передать функцию, которая будет вызвана при удачной отправке формы. Обычно такие методы вызываются на событие form.onsubmit.
* formState содержит актуальную информацию, включая ошибки валидации.

Регистрация поля

Функция register принимает имя поля и объект с настройками валидации и возвращает набор атрибутов для контрола (для отслеживания фокуса и изменений значения). Есть ряд базовых опций, таких как required, minLength, maxLength, pattern. Для каждой настройки можно просто ставить true или конкретное значение (например, минимальная длина или паттерн), а можно передать объект с полями value и message.

Кроме того, можно установить собственную функцию для валидации.

formState

Содержит флаги isSubmitted, isValid, объект с ошибками errors и др.

#ссылки #валидацияформ
👍1👏1
Статус асинхронного запроса

В документации Redux описан хороший паттерн работы с асинхронными запросами. Он не относится непосредственно к Redux, применять его можно где угодно.

Очень часто для отслеживания состояния запроса используют флаги типа isLoading, isLoaded, но намного удобнее иметь дело со строковыми статусами: idle, loading, succeeded, failed. Так проще охватить все возможные состояния и проанализировать их для выведения нужного представления.

Подробнее в документации: https://redux.js.org/tutorials/fundamentals/part-7-standard-patterns#async-request-status

Пример: https://codesandbox.io/s/async-request-status-react-junior-itoqep?file=/src/store/index.js

#redux #документация #паттерны #примерыкода
👏2
Flux Standard Actions

Соглашение по структуре экшенов в Redux:

- экшен - это обычный JavaScript-объект
- у него обязательно есть поле type
- любые данные должны лежать в поле payload
- кроме того у экшена может быть поле meta для различной описательной информации
- при необходимости экшен может иметь поле error (булево значение). Если оно равно true, экшен представляет собой ошибку и является аналогом промиса в состоянии rejected. В этом случае в поле payload должен находиться объект ошибки.

#документация #redux #паттерны
👍2👏1
Нормализация состояния

Во всех предыдущих примерах элементы хранились в store в виде массива. Но в больших проектах со сложным состоянием разумно использовать другую структуру данных - словарь с доступом по идентификатору (обычный JavaScript-объект).

Это способ "нормализовать" состояние.

Цель нормализации состоит прежде всего в том, чтобы избежать копирования данных.

Допустим, у нас есть массив пользователей и массив постов, которые можно лайкать. Для каждого поста нужно сохранять список пользователей, которые его лайкали. Разумнее сохранять только идентификаторы этих пользователей, а не копировать их полностью. Ведь если придется вносить какие-то изменения, их придется вносить в нескольких местах, что увеличивает вероятность ошибки. При необходимости мы всегда можем получить полные данные пользователя, и удобнее это делать из объекта-словаря (время доступа О(1)), чем из массива (время доступа О(n)).

Тут понятно, пользователи и посты мало связаны друг с другом, так что их можно хранить отдельно в разных словарях. Другой пример, возможно, чуть менее очевидный - посты и комментарии к постам. Вроде бы комментарии - часть постов и вполне могут храниться прямо в объекте поста, однако большая вложенность структуры затрудняет любые операции с этими комментариями. Поэтому также разумно хранить комментарии в отдельном словаре с доступом по идентификатору, а в посте - только массив идентификаторов.

При использовании объектов некоторые операции в редьюсере могут стать более громоздкими (например, получение списка). А другие, наоборот, более компактными (например, изменение или удаление элемента). В целом для приложений со сложными данными нормализация более чем окупается.

Раздел в документации (англ.): https://redux.js.org/tutorials/fundamentals/part-7-standard-patterns#normalized-state

#redux #документация #паттерны
👍3
Возращение промиса из thunk

И еще один полезный паттерн, который предлагает нам документация Redux.

Для выполнения асинхронных экшенов мы используем пакет thunk, который позволяет передавать в dispatch функции вместо объектов. Это удобно, например, делать для ajax-запросов к серверу.

Для таких асинхронных запросов часто нужно отслеживать состояние запроса (в процессе/завершен). Мы уже разбирали, что удобно хранить это состояние в виде строки вместо булевых флагов.

Это состояние можно хранить в глобальном хранилище, но все-таки чаще ему место в компоненте.

Например, возьмем обычный TODO-лист. При добавлении нового элемента в список мы отправляем запрос на сервер для его сохранения. Пока этот запрос выполняется, нужно показывать лоадер, а когда запрос завершен, скрывать его.

Уместо поместить логику отображения лоадера в сам компонент, а не в глобальное хранилище. И это сделать очень просто.

Когда мы передаем в dispatch функцию, thunk возвращает результат ее выполнения. Этим результатом вполне может быть промис, ориентируясь на состояни которого мы можем отображать или скрывать лоадер.

Схема работы

https://codesandbox.io/s/thunks-and-promises-react-junior-5edjgl?file=/src/store.js

Функция addNewTodo(text) это создатель экшена, она возвращает thunk вместо обычного объекта.


function addNewTodo(text) {
return function (dispatch) {
return new Promise(function(resolve) {
// запрос на сервер
dispatch(...)
resolve()
})
}
}


При добавлении нового элемента вызываем dispatch(addNewTodo(text)).

В dispatch попадает функция, а не объект, поэтому она сразу же запускается, а результат этого запуска возвращается.


const [ loading, setLoading ] = useState(false)

const handleSubmit = function() {
setLoading(true)
dispatch(addNewTodo(text)).then(function() {
setLoading(false);
})
}


Таким образом, паттерн заключается в том, что из thunk-функции мы возвращаем промис, который используется непосредственно в компоненте.

#redux #управлениесостоянием #примерыкода #паттерны
👍3
Redux Toolkit

Документация Redux очень настойчиво советует использовать в разработке Redux Toolkit, так что я более не могу его игнорировать. Последняя часть руководства Redux Fundamentals как раз посвящена этому инструменту.

Предполагается, что Redux Toolkit позволить упростить работу с хранилищем Redux, а также убережет нас от большинства распространенных ошибок.

Важно: Redux Toolkit - это именно про Redux, то есть про глобальное хранилище. Он не имеет никакого отношения к интерфейсу и связке Redux + React.

По сути, Redux Toolkit - это набор утилит, которые позволяют сократить объем шаблонного кода:

Вот эти утилиты:

- configureStore
- createSlice
- createAsyncThunk
- createEntityAdapter

Будем разбираться с каждой отдельно и сравнивать новый и старый код.

#redux #управлениесостоянием #документация
👍6
Redux Toolkit. Создание хранилища

Первая функция из Redux Toolkit - configureStore - предназначена для создания хранилища.

Вспоминаем, как это происходит в обычном redux:

https://codesandbox.io/s/redux-toolkit-demo-react-junior-4ihz98?file=/src/store.js

1. Объединяем несколько отдельных редьюсеров (слайсов) в один общий rootReducer, который будет обрабатывать каждый экшен (с помощью функции combineReducers).
2. Подключаем thunk, applyMiddleware и composeWithDevTools, чтобы собрать полностью укомплектованное возможностями хранилище.
3. Совмещаем это все в функции createStore.

* Логика внутри слайсов, а также разделение кода по файлам сейчас не имеет значения. Разбираемся только в создании хранилища.

Как выглядит создание хранилища с configureStore:

https://codesandbox.io/s/redux-toolkit-configure-store-react-junior-iv0vpk?file=/src/store.js

1. Передаем функции отдельные редьюсеры - получаем готовое хранилище.

Под капотом происходит объединение редьюсеров-слайсов в один корневой редьюсер, подключение thunk и интеграция с redux-devtools-extension. Помимо этого, добавляются дополнительные миддлвары, которые позволяют отслеживать распространенные ошибки, например, мутации состояния.

Таким образом, мы заменяем три пакета (redux, redux-devtools-extension, redux-thunk) на один (@reduxjs/toolkit).

#redux #паттерны #управлениесостоянием #документация #примерыкода
👍2
👍2🐳1
Redux Toolkit. Создание хранилища. Защита от мутаций

Функция configureStore делает много полезных вещей под капотом, например, подключает redux-thunk и redux-devtools-extensions. Кроме того, она добавляет миддлвар, который отслеживает мутацию стейта в редьюсере.

Если вместо копирования, мы попробуем мутировать текущее состояние, то будет ошибка

// Мутация состояния, Ошибка

state.active = !state.active;
return state;

// Копирование, нет ошибок

return {
...state,
active: !state.active
};


#redux #паттерны #управлениесостоянием #документация #примерыкода
👍2
Redux Toolkit. Слайсы и экшены

Переходим к отдельным редьюсерам, или слайсам (срезам). Будем рассматривать их вместе с экшенами, так как они тесно связаны, ведь редьюсеры обрабатывают экшены.

В обычном redux это довольно многословная часть кода: здесь и конструкция switch, и создатели экшенов.

https://codesandbox.io/s/redux-toolkit-demo-react-junior-4ihz98?file=/src/features/filters/slice.js

В демо-примере эта логика разделена на два файла для каждой фичи - actions.js с типами экшенов и функциями-создателями экшенов и slice.js с редьюсером и селекторами.

1. Определяем типы экшенов.
2. Пишем функции-создатели экшенов.
3. Для каждого типа экшена пишем отдельный кейс в редьюсере.
4. Не забываем передать в редьюсер исходное состояние для инициализации.

Redux Toolkit предлагает функцию createSlice, чтобы упростить эту задачу. Ей нужно передать строку-префикс для типов ("todos" из "todos/addTodo"), начальное состояние и функцию-обработчик для каждого кейса (экшена).

Вместо:

function (state = initialState, action) {
switch(action.type) {
case 'action1'

case 'action2'

default
}
}


Будет вот такой код:

createSlice({
name: 'todos',
initialState,
reducers: {
action1(state, action) {

},
action2(state, action) {

}
}
})


Более декларативно, удобнее читать отдельные кейсы, не нужно указывать избыточный дефолтный кейс.

Более того, в обработчиках можно мутировать параметр state, они не обязаны быть чистыми функциями. При большой вложенности это очень удобно.

Функция createSlice возвращает объект с несколькими полями. Готовый редьюсер находится в поле reducer.

Экшены

Более того, функция createSlice самостоятельно создает action creators для всех указанных типов экшенов. Они находятся в поле actions. Этим функциям-создателям нужно передать то, что должно находиться у экшена в поле payload.

Селекторы

Redux Toolkit самостоятельно импортирует модуль reselect (как хорошую практику работы с redux), поэтому функцию createSelector можно брать прямо из @reduxjs/toolkit.

Обновленный код

https://codesandbox.io/s/redux-toolkit-create-slice-react-junior-v37gp0?file=/src/features/filters/slice.js

В обновленном варианте мы можем избавиться от файла actions.js, так как создатели экшенов генерируются автоматически.

Вот обновленный вариант для слайса todos: https://codesandbox.io/s/redux-toolkit-create-slice-react-junior-v37gp0?file=/src/features/todos/slice.js

Здесь кроме обычных экшенов у нас есть еще один thunk - функция addTodoThunk. Она пока перенесена в старом виде, скоро мы ее заменим.

#redux #паттерны #управлениесостоянием #документация #примерыкода
👍2
👍1
Redux Toolkit. Создатели экшенов

Создатели экшенов автоматически генерируются для всех экшенов, переданных в функцию createSlice.

const slice = createSlice({
name: 'todos',
initialState,
reducers: {
todoAdded(state, action) {},
todoToggled(state, action) {}
}
});

// создатели экшенов находятся здесь
slice.actions; // { todoAdded, todoToggled }

По умолчанию это самые простые функции. Они принимают один аргумент - payload и формируют объект экшена с нужным типом.

// создание экшена 
todoToggled(2); // { type: 'todos/todoToggled', payload: 2 }

Сложные создатели экшенов

Но что если нам нужна функция посложнее, которая может принимать несколько аргументов и собирать из них payload.

Например, для создания новой задачи в списке сейчас мы пишем: todoAdded(data), а хотели бы писать: todoAdded(id, text).

Это можно устроить, немного развернув синтаксис функции createSlice.

Вместо простой функции с аргументами state и action редьюсер можно оформить в виде объекта. В поле reducer передаем эту функцию без изменений, а в поле prepare передаем функцию для формирования тела экшена.

reducers: {
todoAdded: {
reducer(state, action) {},
prepare(id, text) {
return {
payload: {
id,
text
}
}
}
}
}

Кроме payload тут можно указать еще поля meta или error - в соответствии с соглашением Flux Standart Action.

todoAdded(2, 'todo text'); // { type: 'todos/todoAdded', payload: { id: 2, text: 'todo text' }}


#redux #паттерны #управлениесостоянием #документация #примерыкода
👍3👏1
👏1
Redux Toolkit. Создание thunks

Логика создания thunks в Redux Toolkit немного меняется.

Как было?

Раньше функция принимала в качестве аргумента dispatch, сама производила все необходимые действия и сама же вызывала dispatch в нужный момент.

export function addTodoThunk(text) {
return (dispatch) => {
// обработка данных
return createTodo(text)
.then((data) => {
// обращение к хранилищу
dispatch(addTodo(data));
});
};
}


Как стало?

Redux Toolkit позволяет отделить логику обработки данных и логику обращения к хранилищу (ее она берет на себя).

Функция createAsyncThunk принимает два аргумента - имя, на основе которого будут формироваться типы экшенов, и собственно функцию для обработки данных. Эта функция должна возвращать промис, то есть внутри можно производить любые асинхронные действия. Можно использовать async-функции, которые всегда возвращают промис.

export const saveNewTodo = createAsyncThunk(
"todos/saveNewTodo",
async function (text) {
return await createTodo(text);
}
);


Результат вызова createAsyncThunk - это обычный thunk creator. Вызываем его и передаем то, что вернулось, в dispatch.

dispatch(saveNewTodo("SOME TEXT"))


При этом под капотом в хранилище будут диспатчиться дополнительные экшены с типами:

- todos/saveNewTodo/pending (перед вызовом обработчика)
- todos/saveNewTodo/fulfilled (если вызов завершится удачно)
- todos/saveNewTodo/rejected (если будет ошибка)

Для каждого из них автоматически создается action creator:

- saveNewTodo.pending
- saveNewTodo.fulfilled
- saveNewTodo.rejected

Теперь нужно каким-то образом добавить обработку этих новых экшенов в редьюсере - то есть в функцию createSlice.

Добавление асинхронных экшенов в редьюсер

Самая громоздкая и трудная для понимания часть всего процесса.

В функцию createSlice мы передаем обработчики для каждого типа экшена (в поле reducers). И для всех этих типов создаются action creators. Но тут у нас уже есть готовые action creators.

Для таких ситуаций есть еще поле extraReducers. Это функция-коллбэк, которая принимает в качестве аргумента некий builder. И вот в этот билдер с помощью метода addCase нужно добавлять новые экшены с их обработчиками. Первым параметром нужно передать создатель экшена (функцию), вторым - обработчик (тоже функцию).

createSlice({
// ...
extraReducers: function(builder) {
builder.addCase(
saveNewTodo.fulfilled,
function(state, action) {
// изменение state
};
}
})


В следующем посте повторю все это еще раз с примерами из демо-приложения, потому что сложно так сразу огрокать.

#redux #паттерны #управлениесостоянием #документация #примерыкода
👍2
Redux Toolkit. Создание thunks - 2

https://codesandbox.io/s/redux-toolkit-create-async-thunk-react-junior-i9qc3c?file=/src/features/todos/slice.js

1. Асинхронная логика

Итак, сначала пишем собственно асинхронную логику и передаем ее функции createAsyncThunk. Тут у нас запрос на сервер для сохранения новой задачи и получения ее идентификатора:

export const saveNewTodo = createAsyncThunk(
"todos/saveNewTodo",
async (text) => {
return await createTodo(text);
}
);

Помним, что функция должна возвращать промис, а первый аргумент - это префикс для типов экшенов.

В saveNewTodo теперь лежит функция - thunk creator.

Также у нее есть свойства: saveNewTodo.pending, saveNewTodo.fulfilled и saveNewTodo.rejected. В каждом из них лежит простая функция action creator, которая создает экшены с соответствующим типом (todos/saveNewTodo/pending, todos/saveNewTodo/fulfilled).

Нам об этом думать не нужно, эти экшены будут создаваться и диспатчиться автоматически. В .../fulfilled экшен будет также передан результат работы нашей асинхронной функции.

2. Dispatch

Работаем с saveNewTodo как с обычным thunk creator.

dispatch(saveNewTodo(text))


3. Обработка в редьюсере

Чтобы обрабатывать новые экшены в редьюсере, добавляем в createSlice поле extraReducers:

const todosSlice = createSlice({
//...
extraReducers: function(builder) {
builder.addCase(
saveNewTodo.fulfilled,
function(state, action) {
state[action.payload.id] = {
...action.payload,
checked: false
}
}
)
}
})


Тут используем метод builder.addCase(actionCreator, reducer). Можно выстраивать вызовы addCase в цепочку, если требуется обработать несколько экшенов:

builder
.addCase(saveNewTodo.pending, function(state, action) {})
.addCase(saveNewTodo.fulfilled, function(state, action) {})


* В демо примере убран экшен todoAdded, так как вся логика добавления нового элемента в список находится теперь внутри extraReducers.

#redux #паттерны #управлениесостоянием #документация #примерыкода
👍2
Было и стало.

Нельзя, конечно, сказать, что стало короче :) Но зато логика разделена более правильно (+ комментарии).

Обратите внимание, я тут не использую экшен '../pending', потому что состояние запроса отслеживается самим компонентом. Но его вполне можно вынести в общее хранилище и обрабатывать здесь.

#redux #управлениесостоянием #примерыкода #паттерны
👍2
Redux Toolkit. Операции с нормализованным состоянием

И наконец, последняя функция из Redux Toolkit, которую нужно разобрать в этом руководстве - createEntityAdapter. Она упрощает работу с нормализованным состоянием (когда мы храним не массив элементов, а объект, где ключами служат идентификаторы элементов).

Пользоваться очень просто: вызов функции возвращает объект (адаптер) с кучей полезных методов.

const todosAdapter = createEntityAdapter();
/*
{
getInitialState,
addOne,
addMany,
removeOne,
...
}
*/


Демо-проект: https://codesandbox.io/s/redux-toolkit-create-entity-adapter-react-junior-fipune?file=/src/features/todos/slice.js

Начальное состояние

Для получения начального состояния адаптера используем метод adapter.getInitialState. Он вернет объект с полями entities (объект-словарь) и ids (массив идентификаторов).
Чтобы добавить дополнительные поля, нужно просто передать их в функцию:

const initialState = todosAdapter.getInitialState({
status: 'idle'
})


Например, здесь я добавляю статус запроса к серверу. В итоге начальное состояние будет иметь три поля: entities, ids и status.

Манипуляции с данными

Адаптер предоставляет множество полезных методов для работы с коллекцией элементов:

- addOne / addMany
- upsertOne / upsertMany
- updateOne / updateMany
- removeOne / removeMany
- setAll

Их можно использовать в редьюсерах, например, для кейса todoRemoved.

Для наглядности в демо-пример добавлены еще два экшена: completedTodosRemoved и listCleared, которые используют методы адаптера.

Селекторы

Кроме того, адаптер предоставляет два готовых селектора: selectAll и selectById. Чтобы их получить, нужно вызвать метод adapter.getSelectors. Так как этот метод работает с целым состоянием, нужно передать ему функцию для выделения нужно части состояния (в нашем случае state.todos):

const { selectAll, selectById } = todosAdapter.getSelectors(
function(state) {
return state.todos;
}
)


#redux #управлениесостоянием #примерыкода #паттерны #документация
👍1🔥1
Фух, закончилось руководство Redux Fundamentals, разобрались в "низкоуровневых" концепциях redux. Думаю, теперь имеет смысл пройти Redux Essentials. Документация уверяет, что в этом руководстве больше внимания уделяется тому, как пишутся приложения "in real world".

Вероятно, какая-то информация там будет повторяться, так что, может, получится двигаться быстрее)

#redux #управлениесостоянием #документация
👏4👍1