Сделайте React более реактивным с помощью RxJs Observer: Шаблон проектирования
Привет, разработчики, мы рассмотрим статью об использовании шаблонов проектирования в React.
И сегодня мы поговорим о шаблоне проектирования Observer в контексте React.
Короче говоря, паттерн проектирования Observer — это поведенческий паттерн, который позволяет некоторым объектам уведомлять другие объекты об изменениях в их состоянии. Этот шаблон работает по модели подписки, что означает, что подписчик, обычно называемый наблюдателем, подписывается на событие или действие, которое обрабатывается издателем, обычно называемым субъектом.
Когда событие или действие происходит, подписчик уведомляется издателем. Думайте об этом как о подписке на информационный бюллетень или канал YouTube. Когда появляется новый контент, вы получаете уведомление и можете выбрать, использовать ли этот контент.
Angular по умолчанию использует шаблон проектирования Observer, адаптируя RxJs к фреймворку. RxJS — это библиотека, которая реализует шаблон проектирования Observer с реактивным программированием. В этой статье мы не будем углубляться в библиотеку RxJs, а скорее найдем несколько творческих идей о том, как ее можно использовать в вашем приложении React, чтобы сделать его еще более реактивным. Прилагаем дополнительно для вас статью о том, как сделать динамическое приглашение с React при каждом обновлении вашего проекта.
Улучшение обработки событий в React с помощью Observer
Представьте, что вы создаете веб-сайт электронной коммерции, и вам нужно отобразить список продуктов на основе поискового запроса пользователя. Для этого вы можете использовать библиотеку RxJS для создания наблюдаемого потока, который обновляется в режиме реального времени по мере того, как пользователь вводит свой поисковый запрос.
С RxJS вы можете использовать функцию fromEvent
для создания наблюдаемого объекта, который выдает значение поля ввода поиска каждый раз, когда пользователь вводит новый символ. Затем вы можете использовать такие операторы, как debounceTime
, DifferentUntilChanged
и switchMap
, для управления потоком данных и обеспечения отображения пользователю только самых последних и релевантных данных.
const getProductsBySearch = () => {
return Promise.resolve([
{ id: "1", name: "Bob" },
{ id: "2", name: "John" }
]);
};
function ProductList() {
const [products, setProducts] = useState([]);
const searchInputRef = useRef(null);
useEffect(() => {
const searchInput = searchInputRef.current;
const search$ = fromEvent(searchInput, "input").pipe(
debounceTime(500),
map((event) => event.target.value),
distinctUntilChanged(), // filters out if new value is not unique
switchMap((query) => { // change stream from input value to search response
if (query.trim() === "") {
return of([]);
} else {
return getProductsBySearch(query);
}
})
);
const subscription = search$.subscribe((products) => {
setProducts(products);
});
return () => {
subscription.unsubscribe();
};
}, []);
return (
<div>
<input type="text" id="searchInput" ref={searchInputRef} />
<ul>
{products.map((product) => (
<li key={product?.id}>{product?.name}</li>
))}
</ul>
</div>
);
}
В этом примере мы создаем наблюдаемый search$
, который прослушивает поисковый ввод пользователя, ждет 500ms между нажатиями клавиш и выдает новый поисковый запрос, только если он изменился. Затем мы используем операторы RxJS для переключения на новый наблюдаемый поток, который извлекает продукты, соответствующие поисковому запросу.
Наконец, мы подписываемся на наблюдаемый search$
и обновляем состояние компонента с новым списком продуктов. Таким образом, список продуктов будет автоматически обновляться в режиме реального времени по мере того, как пользователь вводит свой поисковый запрос.
RxJS использует концепции функционального программирования для создания цепочки и изменения потока. Это дает много возможностей, особенно при использовании библиотечных операторов. Позволит вам управлять паром по-разному.
Использование RxJs в React для проверки сложной формы
Еще один очень мощный пример использования RxJs в качестве наблюдаемого шаблона в React — проверка формы. В нашем сценарии у нас есть три разных поля формы: name
, email
и password
. У каждого из них есть условие проверки, и функция validateInput
проверит, действительно ли поле ввода, чтобы показать ошибку. Вот как выглядит код:
const submitFormToServer = () => Promise.resolve({});
function validateInput(name, value) {
let error = "";
if (name === "name") {
if (!value) {
error = "Name is required";
} else if (value.length < 3) {
error = "Name must be at least 3 characters";
}
} else if (name === "email") {
if (!value) {
error = "Email is required";
} else if (!/\S+@\S+\.\S+/.test(value)) {
error = "Email is invalid";
}
} else if (name === "password") {
if (!value) {
error = "Password is required";
} else if (value.length < 8) {
error = "Password must be at least 8 characters";
}
}
return error;
}
function Form() {
const [formData, setFormData] = useState({});
const [formErrors, setFormErrors] = useState({});
const nameRef = useRef(null);
const emailRef = useRef(null);
const passwordRef = useRef(null);
const submitRef = useRef(null);
useEffect(() => {
const subscription = new Subscription();
const name$ = fromEvent(nameRef.current, "input").pipe(
pluck("target", "value"), // pluck will distuct the value from the object
map((value) => ({ name: "name", value }))
);
const email$ = fromEvent(emailRef.current, "input").pipe(
pluck("target", "value"),
map((value) => ({ name: "email", value }))
);
const password$ = fromEvent(passwordRef.current, "input").pipe(
pluck("target", "value"),
map((value) => ({ name: "password", value }))
);
const submit$ = fromEvent(submitRef.current, "click").pipe(
startWith(null),
mapTo({ isSubmit: true })
);
const form$ = combineLatest(name$, email$, password$); // combine latest will emit the value when each of the observer is completed
const formSub = merge(name$, email$, password$) // merge will emit when any is changed
.pipe(
tap((input) => {
const { name, value } = input;
setFormData((data) => ({ ...data, [name]: value }));
setFormErrors((errors) => ({
...errors,
[name]: validateInput(name, value)
}));
})
)
.subscribe();
const submitSub = submit$
.pipe(withLatestFrom(form$), tap(([, formData]) => {
const hasError = formData.some(
({ name, value }) => !!validateInput(name, value)
); // check if any field has error
if(!hasError){
submitFormToServer(formData).then();
}
}))
.subscribe();
subscription.add(formSub); // we can add multiple subscription to the Subscription object, to unsubscribe in one call
subscription.add(submitSub);
return () => subscription.unsubscribe();
}, []);
return (
<div>
<input type="text" id="name" placeholder="Name" ref={nameRef} />
{formErrors.name && <span>{formErrors.name}</span>}
<br />
<input type="text" id="email" placeholder="Email" ref={emailRef} />
{formErrors.email && <span>{formErrors.email}</span>}
<br />
<input
type="password"
id="password"
placeholder="Password"
ref={passwordRef}
/>
{formErrors.password && <span>{formErrors.password}</span>}
<br />
<button type="button" id="submit" ref={submitRef}>
Submit
</button>
</div>
);
}
В приведенном примере мы строим довольно сложную логику, используя RxJs Observers. Мы можем еще больше улучшить его, добавив дополнительные операторы такие как differentUntilChanged
, выдающие только в том случае, если новое значение уникально, или debounceTime
, чтобы улучшить производительность проверки ввода. RxJs дает нам большую гибкость, единственное, не забывайте отписываться, чтобы очистить ваши наблюдаемые.
Использование RxJs для обработки состояния в React
RxJs могут улучшить обработку состояния в React, чтобы добавить дополнительный контроль и мощность операторов RxJs. Давайте посмотрим пример.
Допустим, у нас есть чат-приложение, в котором пользователи могут отправлять и получать сообщения в режиме реального времени. Обычно нам нужно создавать переменные состояния для списка сообщений и обновлять их всякий раз, когда новое сообщение отправляется или принимается. Однако с RxJs мы можем использовать Observables для прослушивания новых сообщений и обновления списка в режиме реального времени.
function Chat() {
const [message, setMessage] = useState('');
const [messages, setMessages] = useState([]);
const [message$] = useState(new Subject()); // Subject creates empty Observable
// Note, we need to use useState for our observable to trigger reconciliation after calling message$.next
useEffect(() => {
const messages$ = message$.pipe(
scan((acc, curr) => [...acc, curr], []) // scan lets you dynamically update the state in Redux fashion
);
const subscription = messages$.subscribe(newMessages => {
setMessages(newMessages);
});
return () => {
subscription.unsubscribe();
};
}, []);
const handleMessageSend = () => {
message$.next({
user: 'Me',
text: message,
timestamp: Date.now(),
});
setMessage('');
};
return (
<div>
<ul>
{messages.map((msg, i) => (
<li key={i}>
{msg.user}: {msg.text}
</li>
))}
</ul>
<input type="text" value={message} onChange={e => setMessage(e.target.value)} />
<button onClick={handleMessageSend}>Send</button>
</div>
);
}
Здесь мы создаем новую тему с именем message$
для обработки входящих сообщений. Мы также создаем новый Observable с именем messages$
, используя оператор scan()
для накопления входящих сообщений в массив.
В хуке useEffect
мы подписываемся на messages$
Observable и обновляем список сообщений всякий раз, когда приходит новое сообщение. Когда компонент размонтирован, мы отписываемся от Observable, чтобы предотвратить утечку памяти.
В функции handleMessageSend
мы отправляем новое сообщение, вызывая message$.next()
с новым объектом сообщения, содержащим текст сообщения, имя пользователя и отметку времени. Мы также очищаем поле ввода сообщения, вызывая setMessage('')
.
Используя RxJs для обработки состояния таким образом, мы можем легко обрабатывать обновления списка сообщений в реальном времени без необходимости вручную обновлять переменную состояния. Кроме того, RxJs предоставляет мощные операторы, которые можно использовать для преобразования значений состояния и управления ими в режиме реального времени, что делает его мощным инструментом для управления сложным состоянием в приложениях React.
RxJs и Custom Hook — лучший способ проектирования состояния React
Взяв предыдущий пример, мы рискуем открыть компоненту слишком много логики. Это может привести к ускорению кода компонента, особенно когда мы имеем дело со многими наблюдаемыми. Лучший дизайн — извлечь наше управляемое состояние RxJs в Custom Hook, давайте посмотрим на пример.
function useMessageObservable() {
const [messages, setMessages] = useState([]);
const [message$] = useState(new Subject()); // Subject creates empty Observable
// Note, we need to use useState for our observable to trigger reconciliation after calling message$.next
useEffect(() => {
const messages$ = message$.pipe(
scan((acc, curr) => [...acc, curr], []) // scan lets you dynamically update the state in Redux fashion
);
const subscription = messages$.subscribe(newMessages => {
setMessages(newMessages);
});
return () => {
subscription.unsubscribe();
};
}, []);
return {message$, messages};
}
Позже мы можем обновить наш компонент Chat:
function Chat() {
const [message, setMessage] = useState('');
const {message$, messages} = useMessageObservable()
const handleMessageSend = () => {
message$.next({
user: 'Me',
text: message,
timestamp: Date.now(),
});
setMessage('');
};
return (
<div>
<ul>
{messages.map((msg, i) => (
<li key={i}>
{msg.user}: {msg.text}
</li>
))}
</ul>
<input type="text" value={message} onChange={e => setMessage(e.target.value)} />
<button onClick={handleMessageSend}>Send</button>
</div>
);
}
Таким образом, мы инкапсулируем наше наблюдаемое состояние.
Redux, Context + RxJs Observable в React
Чтобы лучше понять, можем ли мы получить какие-либо преимущества от использования RxJs Observable в связке с Redux в React, мы должны прояснить основную идею Redux.
Redux — это библиотека управления состоянием, которая широко используется в приложениях React. Состояние часто разбросано по нескольким компонентам и может стать сложным и трудным для управления по мере роста приложения. Redux помогает решить эту проблему, предоставляя единый источник достоверной информации о состоянии приложения, к которому может получить доступ любой компонент.
Другими словами, он служит для совместного использования и кэширования состояния, давайте пока запомним это утверждение. Позже в React был представлен Context
, и вместе с useReducer
хук постепенно заменяет цель использования Redux. Так как делал то же самое, что и библиотека Redux, но нативно, без добавления дополнительной библиотеки. Итак, где RxJs Observable стоит в концепции управления состоянием. Мы бы предпочел думать об этом как об улучшении управления состоянием, а не как о замене.
Итак, чтобы улучшить наше управление состоянием, можно просто интегрировать RxJs Observable в контекст, мы можем взять наш предыдущий пример.
// Define a context to expose the message state to child components
const MessageContext = createContext();
// Define a provider component that listens for messages
function MessageProvider({ children }) {
// Define the initial state of the chat
const {message$, messages} = useMessageObservable()
return <MessageContext.Provider value={{message$, messages}}>{children}</MessageContext.Provider>;
}
После включения нашего родительского компонента в MessageProvider
мы можем получить доступ к контексту состояния из его дочерних элементов:
function Chat() {
const [message, setMessage] = useState('');
const {message$, messages} = useContext(MessageContext);
...
}
Заключение
Изучая некоторые творческие идеи, вы можете использовать этот шаблон в своем приложении React, чтобы сделать его еще более реактивным. Мы предоставляем еще одну статью на тему отправки кроссплатформенных нативных уведомлений в Node JS для вашего ознакомления, тема тоже довольно интересна. Несмотря на то, что библиотека RxJs активно используется в Angular, это не значит, что вы не можете использовать лучшее от конкурента React.