Я пытаюсь создать динамическую форму, в которой поля ввода формы отображаются на основе данных, возвращаемых API.
Поскольку у атома должен быть уникальный ключ, я попытался обернуть его внутри функции, но каждый раз, когда я обновляю значение поля или компонент перемонтируется (попробуйте изменить вкладки), я получаю предупреждение:
Я сделал небольшой рабочий пример здесь https://codesandbox.io/s/zealous-night-e0h4jt?file=/src/App.tsx (тот же код, что и ниже):
import React, { useEffect, useState } from "react";
import { atom, RecoilRoot, useRecoilState } from "recoil";
import "./styles.css";
const textState = (key: string, defaultValue: string = "") =>
atom({
key,
default: defaultValue
});
const TextInput = ({ id, defaultValue }: any) => {
const [text, setText] = useRecoilState(textState(id, defaultValue));
const onChange = (event: any) => {
setText(event.target.value);
};
useEffect(() => {
return () => console.info("TextInput unmount");
}, []);
return (
<div>
<input type = "text" value = {text} onChange = {onChange} />
<br />
Echo: {text}
</div>
);
};
export default function App() {
const [tabIndex, setTabIndex] = useState(0);
// This would normally be a fetch request made by graphql or inside useEffect
const fields = [
{ id: "foo", type: "text", value: "bar" },
{ id: "hello", type: "text", value: "world" }
];
return (
<div className = "App">
<RecoilRoot>
<form>
<button type = "button" onClick = {() => setTabIndex(0)}>
Tab 1
</button>
<button type = "button" onClick = {() => setTabIndex(1)}>
Tab 2
</button>
{tabIndex === 0 ? (
<div>
<h1>Fields</h1>
{fields.map((field) => {
if (field.type === "text") {
return (
<TextInput
key = {field.id}
id = {field.id}
defaultValue = {field.value}
/>
);
}
})}
</div>
) : (
<div>
<h1>Tab 2</h1>Just checking if state is persisted when TextInput
is unmounted
</div>
)}
</form>
</RecoilRoot>
</div>
);
}
Возможно ли это даже с отдачей. Я имею в виду, что это работает, но я не могу игнорировать предупреждения.
Думаю проблема в textState(id, defaultValue)
. Каждый раз, когда вы запускаете повторный рендеринг для TextInput
, эта функция будет вызываться снова, чтобы создать новый атом с тем же ключом.
Чтобы избежать такой ситуации, вы можете создать глобальную переменную для отслеживания добавленных atom
. Например
let atoms = {}
const textState = (key: string, defaultValue: string = "") => {
//if the current key is not added, should add a new atom to `atoms`
if (!atoms[key]) {
atoms[key] = atom({
key,
default: defaultValue
})
}
//reuse the existing atom which is added before with the same key
return atoms[key];
}
Этот ответ показывает, как вы можете вручную управлять несколькими экземплярами атомов с помощью мемоизации.
Однако, если ваш defaultValue
для каждого экземпляра использования не изменится, то Recoil уже предоставляет утилиту, которая может позаботиться об этом создании и запоминании для вас: atomFamily
. Я процитирую некоторую соответствующую информацию из предыдущей ссылки (но прочитайте все, чтобы полностью понять):
... You could implement this yourself via a memoization pattern. But, Recoil provides this pattern for you with the
atomFamily
utility. An Atom Family represents a collection of atoms. When you callatomFamily
it will return a function which provides theRecoilState
atom based on the parameters you pass in.The
atomFamily
essentially provides a map from the parameter to an atom. You only need to provide a single key for theatomFamily
and it will generate a unique key for each underlying atom. These atom keys can be used for persistence, and so must be stable across application executions. The parameters may also be generated at different callsites and we want equivalent parameters to use the same underlying atom. Therefore, value-equality is used instead of reference-equality foratomFamily
parameters. This imposes restrictions on the types which can be used for the parameter.atomFamily
accepts primitive types, or arrays or objects which can contain arrays, objects, or primitive types.
Вот рабочий пример, показывающий, как вы можете использовать свои id
и defaultValue
(уникальная комбинация значений в виде кортежа) в качестве параметра при использовании экземпляра состояния atomFamily
для каждого ввода:
body { font-family: sans-serif; }
input[type = "text"] { font-size: 1rem; padding: 0.5rem; }
<div id = "root"></div><script src = "https://unpkg.com/[email protected]/umd/react.development.js"></script><script src = "https://unpkg.com/[email protected]/umd/react-dom.development.js"></script><script src = "https://unpkg.com/[email protected]/umd/recoil.min.js"></script><script src = "https://unpkg.com/@babel/[email protected]/babel.min.js"></script><script>Babel.registerPreset('tsx', {presets: [[Babel.availablePresets['typescript'], {allExtensions: true, isTSX: true}]]});</script>
<script type = "text/babel" data-type = "module" data-presets = "tsx,react">
// import ReactDOM from 'react-dom';
// import type {ReactElement} from 'react';
// import {atomFamily, RecoilRoot, useRecoilState} from 'recoil';
// This Stack Overflow snippet demo uses UMD modules instead of the above import statments
const {atomFamily, RecoilRoot, useRecoilState} = Recoil;
const textInputState = atomFamily<string, [id: string, defaultValue?: string]>({
key: 'textInput',
default: ([, defaultValue]) => defaultValue ?? '',
});
type TextInputProps = {
id: string;
defaultValue?: string;
};
function TextInput ({defaultValue = '', id}: TextInputProps): ReactElement {
const [value, setValue] = useRecoilState(textInputState([id, defaultValue]));
return (
<div>
<input
type = "text"
onChange = {ev => setValue(ev.target.value)}
placeholder = {defaultValue}
{...{value}}
/>
</div>
);
}
function App (): ReactElement {
const fields = [
{ id: 'foo', type: 'text', value: 'bar' },
{ id: 'hello', type: 'text', value: 'world' },
];
return (
<RecoilRoot>
<h1>Custom defaults using atomFamily</h1>
{fields.map(({id, value: defaultValue}) => (
<TextInput key = {id} {...{defaultValue, id}} />
))}
</RecoilRoot>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
</script>
Спасибо и за ваш ответ. Я бы очень не решился полагаться на глобальные переменные в реагирующих приложениях, но, по крайней мере, это выполняет свою работу. Подход atomFamily кажется лучшим решением.