Если я чему-то научился, будучи приклеенным к редактору кода в течение (вероятно, слишком многих) лет, так это тому, что самый простой подход почти всегда лучший. 1.
В случае современной фронтенд-инженерии и особенно React, вы можете свести все к двум простым концепциям …
- Визуализация текущего состояния
- Обновление состояния
На самом деле это немного сложнее. Вам часто нужно учитывать общее состояние приложения и состояние внутреннего компонента. Обновления также могут происходить асинхронно с потенциально множеством различных состояний по пути, возможно, достигая неожиданного состояния, когда возникает ошибка или если пользователь прерывает асинхронную операцию. Возможно, вам также придется учитывать производительность при рендеринге большого количества данных или анимации сложных сцен.
Но когда все сводится к одному, все является просто функцией состояния.
Имея это в виду , у нас есть пара супер простых хуков React, которые мы используем везде. В сочетании с шаблоном проектирования, который мы рассмотрим позже в этом сообщении в блоге, трудно представить, что для подавляющего большинства случаев потребуется гораздо больше. Мы по-прежнему используем базовые хуки, такие как useState
useEffect
useMemo
и useRef
по мере необходимости, но что касается сложного государственного управления, обычно все, что нам нужно, — это следующие два пользовательских крючка.
Содержание статьи
useAsyncExtendedState
Этот хук работает почти так же, как встроенный в React хук useState
но с двумя дополнениями. Это, вероятно, легче всего понять на примере.
-
Он включает дополнительный метод для расширения текущего состояния.
Определите интерфейс
Состояние
.]
интерфейс Государство {
foo : строка
бар : строка
}
Используйте крючок.
const [ состояние setState extendState ] = useAsyncExtendedState < Состояние > ( {
foo : ` foo `
бар : ` бар `
} )
Настройка состояния работы как обычно.
setState ( { foo : ` Здравствуйте ` bar : ` Мир! ` } )
setState ( состояние => ( { foo : ` Здравствуйте ` бар : ` Мир! ` } ) )
Или вы можете расширить государство.
extendState ( { foo : ` Здравствуйте ` } )
extensionState ( состояние => ( { foo : состояние . foo . toUpperCase ( ) } ) )
Дон t используйте
extendState
для всего! Используйте его только тогда, когда знаете, что вам нужно объединить частичное состояние. Это синтаксический сахар, делающий ваш код немного более лаконичным.С помощью Molecule.dev мы стараемся дать вам как можно больший контроль над кодом вашей Molecule. Весь этот код имеет открытый исходный код, поэтому вы можете быстро изменить его в соответствии со своими потребностями и предпочтениями.
-
Вы можете передавать обещания как
setState
так иextendState
для асинхронного обновления состояния.Предположим, у вас есть функция, которая асинхронно извлекает данные из вашего API и возвращает данные в виде следующего состояния.
const fetchState = ( ) => API . клиент . получить < Государство > ( ` данные ` ) . затем ( ответ => { ]
возврат ответ . данные
} )
]
В качестве альтернативы, обещание может также разрешить функцию текущего состояния, аналогично
setState (state => ({... state}))
.const fetchState = ] ( ) => API . клиент . получить < Государство > ( ` данные ` ) . затем ( ответ => {
возврат ( состояние : Государство ) => {
возврат ответ . данные
}
} )
Вызвать асинхронную функцию и передать обещание
setState
. В конечном итоге для состояния будет установлено разрешенное значение.setState ( fetchState ( ) )
Аналогично для
extendState
предположим, что у вас есть функция, которая асинхронно извлекает частичные данные из вашего API.const ] fetchPartialState = ( ) => API . клиент . получить < Частично < Государство >> ( ` данные ` ) . затем ( ответ => {
возврат ответ . данные
} )
const fetchPartialState = ( ) => API . клиент . получить < Частично < Государство >> ( ` данные ` ) . затем ] ( ответ => {
возврат ( состояние : Государство ] ) => {
возврат ответ . данные
}
} )
Вызовите
fetchPartialState
и передайте обещаниеextendState
. Состояние в конечном итоге будет расширено на разрешенное значение.extendState ( fetchPartialState ( ) )
usePromise
Чаще всего, когда что-то происходит асинхронно, вы захотите отобразить некоторую обратную связь с пользователем.
Этот хук принимает любую асинхронную функцию (т. е. функцию, которая возвращает обещание) и возвращает новую асинхронную функцию, обертывающую оригинал, вместе с состоянием обещания плюс методы cancel
и reset
.
Например, предположим, что мы хотим получить данные из API и отобразить текущее состояние запроса для пользователя.
Определите Интерфейс состояния
.
интерфейс Государство ] {
foo : строка
бар : строка
] }
Определите функцию, которая возвращает обещание. В этом примере мы извлекаем данные API и разрешаем их как Состояние
.
const читать = ( id : строка ) => API . клиент . получить < Государство > ( ` вещи / $ { id } ` ) . затем ( ответ => {
возврат ответ . данные
] } )
Pass функция для крючка usePromise
.
const [ ] readThingRequest readThing ] = usePromise ( читать )
]
readThingRequest
содержит состояние обещания плюс метод reset
de оштрафован ниже:
интерфейс PromiseState < T > {
статус ? ] : ` в ожидании ` | ` решено ` | ` отклонено `
обещание ? : Обещание < T >
v alue ? : Ожидается < T >
ошибка ? : Ошибка
отменить ? : ( сообщение ] ? : строка ) => пусто
}
тип PromiseStateWithReset < T > = PromiseState < T > и {
сброс : ( ключи ? : Массив < keyof PromiseState < T >> ) => пусто
}
Если вы хотите инициализировать прочтите ThingRequest
передайте начальное состояние в качестве второго аргумента:
const readThingRequest readThing ] = usePromise ( читать {
статус : ` разрешено ` ]
значение : состояние
} )
readThing
имеет ту же функциональную сигнатуру, что и read
. Другими словами, вы вызываете readThing (id)
так же, как read (id)
.
Первоначально, до readThing
readThingRequest
будет пустым объектом (кроме метода reset
), если исходное состояние не было предоставлено.
Когда вы вызываете readThing (id)
состояние readThingRequest
становится:
const readThingRequest = {
статус : ` в ожидании `
обещание
отменить
сбросить
}
Когда обещание выполнено, readThingRequest
состояние становится следующим:
const readThingRequest = ] {
статус : ` разрешено «
значение
сброс
}
Если произошла ошибка и обещание отклонено:
const readThingRequest = {
статус : ` отклонено `
ошибка
сброс
}
]
Если readThingRequest.cancel
был вызван до выполнения обещания, то readThingRequest
состояние сразу становится следующим:
const readThingRequest = {
статус : ` отклонено `
ошибка
сброс
}
Чтобы отменить обновления для текущего обещания без сообщения об ошибке:
readThingRequest . отменить ( ) [1 9459023]
Чтобы отменить обновления для текущего обещания, с сообщением об ошибке:
readThingRequest . отменить ( 'Отменено!' )
Примечание. Отмена только предотвращает обновление состояния. Вызванная вами асинхронная функция будет продолжаться, если у вас не будет способа ее остановить. Например, в нашем случае, когда мы используем
axios
для запросов API, нам нужно будет включить его методы отмены. Однако это выходит за рамки данной статьи, посколькуusePromise
может использоваться для любой асинхронной операции.
Чтобы полностью сбросить состояние:
readThingRequest . сброс ( )
Или, если вы хотите сбросить определенное значение ( readThingRequest.error
например ):
readThingRequest . сброс ( ] ` ошибка ` )
Или несколько (оба readThingRequest.status
и readThingRequest.error
например):
readThingRequest . reset ( [ ` статус ` ` ошибка ` ] )
Вы также можете вызвать readThing (id)
более одного раза, и в этом случае текущее readThingRequest.value
останется до он заменяется следующим разрешенным значением. Итак, если вы хотите сбросить значение при повторной выборке:
readThingRequest . reset ] ( [ ` значение ` ] )
readThing ( id )
Примечание. Если вы комбинируете
readThing
с хукомuseAsyncExtendedState
вы вероятно, наплевать наreadThingRequest.value
. Подробнее об этом ниже.
Объединение двух крючков
Крюк usePromise
особенно хорошо сочетается с хуком useAsyncExtendedState
.
]
В начале этого поста я упомянул шаблон проектирования, который делает все чистым и предсказуемым. Мы использовали его в приведенных выше примерах, но теперь мы сделаем его более очевидным.
Ресурсы API обычно имеют несколько собственных маршрутов, обычно соответствующих RESTful-дизайну с CRUD (create , читать, обновлять и удалять) методы.
Основной код приложения Molecule.dev структурирован таким образом, что каждый ресурс API имеет свой собственный каталог с индексом, который экспортирует методы для каждого маршрута.
Предположим, у нас есть ресурс API под названием вещь
с методами CRUD:
src / API / resource / thing.ts
импорт { клиент } из ] '../../ клиент'
импорт * как [1 9459018] типы из './ types'
]
экспорт const создать = ( props : типы . CreateProps ) : Promise < типы . SuccessResponse > => (
клиент . post ( ` вещи ` опоры )
)
]
экспорт const читать = ( id : строка ) : Promise<types.SuccessResponse> => (
client.get(`things/${id}`)
)
export const update = (id: string, props: types.UpdateProps): Promise<types.SuccessPartialResponse> => (
client.patch(`things/${id}`, props)
)
export const del = (id: string): Promise<types.SuccessPartialResponse> => (
client.delete(`things/${id}`)
)
We'll import these methods and use them with the useAsyncExtendedState
and usePromise
hooks.
As an example, let's create a component to read some thing
by id and allow the user to update it.
First, let's cover some relevant type definitions.
Every RESTful API resource will have an id
createdAt
date, and updatedAt
date:
src/API/resource/types.ts
export interface Props {
id: string
createdAt: string
updatedAt: string
}
Our thing
resource will have a description
property:
src/API/resource/thing/types.ts
import * as resourceTypes from '../types'
[1 9459018]export interface Props extends resourceTypes.Props {
description?: string
}
We've also defined types for every API request and response, which you can check out on GitHub.
For the sake of example, we'll create an Edito r
component with a predefined initial state and render the current state with an input for updating the description
. An "Update thing" button will request an API update when clicked, extending the state with the response data, and we'll render a cancel button along with the current request status and error, if defined.
import React from 'react'
import { useAsyncExtendedState, usePromise } from '../../hooks'
import { update } from '../../API/resource/thing'
import { types } from '../../API/resource/thing'
export const Editor = () => {
const [ state, setState, extendState ] = useAsyncExtendedState<types.Props>({
id: `733e26aa-97ea-46d7-b4d4-e556a5f37d68`,
createdAt: `2021-12-17T12:32:13.981Z`,
updatedAt: `2021-12-17T12:32:13.981Z`,
description: `This is a simple but powerful design pattern!`
})
const [ updateThingRequest, updateThing ] = usePromise((updateProps: types.UpdateProps) => (
update(state.id, updateProps).then(response => response.data.props)
))
return (
<div>
<div>
{`Created thing: ${new Date(state.createdAt).toLocaleString()}`}
</div>
<div>
{`Updated thing: ${new Date(state.updatedAt).toLocaleString()}`}
</div>
<textarea
value={state.description}
onChange={event => extendState({ description: event.target.value })}
/>
<button onClick={() => extendState(updateThing({ description: state[1 9459081].description }))}>
{updateThingRequest.status === `pending` ? `Updating thing...` : `Update thing`}
</button>
{updateThingRequest.cancel && (
<button onClick={() => updateThingRequest.cancel(`cancelled`)}>
Cancel update
</button>
)}
{updateThingRequest.status && (
<div>
{`Request status: ${updateThingRequest.status}`}
</div>
)}
{updateThingRequest.error && (
<div style={{ color: `red` }}>
{`Error: ${updateThingRequest.error .message}`}
</div>
)}
</div>
)
}
Check out the source
You can find the code for these hooks on our GitHub here.
If you want to get started on an app and API using these design patterns, git clone molecule-app
and molecule-api
. Thorough documentation and guides are included.
git clone https://github.com/Molecule-dev/molecule-app.git
cd molecule-app
npm install
git clone https://github.com/Molecule-dev/molecule-api.git
cd molecule-api
npm install
See it in action
Created thing: 12/17/2021, 12:32:13 PM
Updated thing: 12/17/2021, 12:32:13 PM
This example is also available to play with on CodeSandbox.
Conclusion
You can combine these two hooks to cleanly manage asynchronous state at any level throughout your app, for nearly anything you can think of.
These design patterns help you simplify your thinking and the code itself by separating concerns in a way that is predictably consistent and incredibly easy to manage and build upon. This naturally leads to both a better user experience and a better developer experience.
-
Define asynchronous functions on their own.
-
Asynchronously set or extend state with a one-liner, via the
useAsyncExtendedState
hook. -
To show the user the current state of any asynchronous operation, pass the async function to the
usePromise
hook and render the promise state.
We use this for everything throughout applications built with Molecule.dev — signing up, logging in, the users themselves, enabling 2FA, payments, subscriptions, plan changes, device management, push notifications… everything!
If you like this and want to learn more, check out our GitHub and get started building something of your own by cloning Molecule.dev's core TypeScript app and API. After installing Node dependencies, you'll be greeted with documentation generated by TypeDoc where you can dig into more internals and play around.
If you're a professional web developer, where you're an indie dev or a CTO, and you want to save months of development time on cross-platform apps, visit Molecule.dev to assemble a codebase tailored to your specific needs. You're guaranteed a rock solid foundation from which you and your team can scale with ease.
Also be sure to follow us on Twitter @molecule_dev for regular updates and more posts like this one!