我想将 React 组件存储在 React 状态,但我认为这在技术上是错误的。对此你有何感想?
示例
const LayoutContext = createContext(null as unknown as {
setSidebarContent: React.Dispatch<React.SetStateAction<null | React.ReactNode>>;
})
const Layout = () => {
const [sidebarContent, setSidebarContent] = useState<null | React.ReactNode>(
null,
);
return (
<LayoutContext.Provider value={{
setSidebarContent
}}>
<div>
<Sidebar>
{sidebarContent}
</Sidebar>
<div className='page-container'>
<Suspense fallback={<div>Loading...</div>}>
<Outlet
</Suspense>
</div>
</div>
</LayoutContext.Provider>)
};
在这个带有
LayoutContext
的示例中,我为侧边栏内容提供了设置器,我想知道它是否会导致该方法出现一些问题,以及是否还有其他方法可以从子组件设置内容?
@lorweth333 在技术上是正确的,但有严重的陷阱需要注意 -
function App() {
const [state, setState] = React.useState(<Counter />)
return <div>
{state} click the counter ✅
<hr />
<button onClick={() => setState(<Counter />)} children="A" />
<button onClick={() => setState(<Counter init={10} />)} children="B" />
<button onClick={() => setState(<Counter init={100} />)} children="C" />
change the counter ❌
</div>
}
function Counter(props) {
const [state, setState] = React.useState(props.init || 0)
return <button onClick={() => setState(_ => _ + 1)} children={state} />
}
ReactDOM.createRoot(document.querySelector("#app")).render(<App />)
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div id="app"></div>
程序运行,但从一种组件状态更改为另一种组件状态并没有像我们预期的那样工作。 React 检测到 App 中的状态发生了变化,但是当渲染
{state}
时,React 不知道这是一个 new Counter 组件,与任何其他组件不同。因此呈现相同的计数器并且状态保持不变。
如果您重新
key
包含存储在状态中的组件的元素,您可以提示初始化一个新的计数器。这显然是一个hack并且应该避免。这个演示只是为了更好地了解 React 如何“看到”事物 -
function App() {
const [state, setState] = React.useState(<Counter />)
return <div key={Math.random() /* hack ❌ */ }>
{state} click the counter ✅
<hr />
<button onClick={() => setState(<Counter />)} children="A" />
<button onClick={() => setState(<Counter init={10} />)} children="B" />
<button onClick={() => setState(<Counter init={100} />)} children="C" />
change the counter ✅
</div>
}
function Counter(props) {
const [state, setState] = React.useState(props.init || 0)
return <button onClick={() => setState(_ => _ + 1)} children={state} />
}
ReactDOM.createRoot(document.querySelector("#app")).render(<App />)
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div id="app"></div>
现在我们可以通过 React Vision 看到,我们可以理解为什么为每个 Counter 组件添加一个唯一的
key
也会“解决”问题 -
function App() {
const [state, setState] = React.useState(<Counter key={0} />)
return <div>
{state} click the counter ✅
<hr />
<button onClick={() => setState(<Counter key={0} />)} children="A" />
<button onClick={() => setState(<Counter key={1} init={10} />)} children="B" />
<button onClick={() => setState(<Counter key={2} init={100} />)} children="C" />
change the counter ✅
</div>
}
function Counter(props) {
const [state, setState] = React.useState(props.init || 0)
return <button onClick={() => setState(_ => _ + 1)} children={state} />
}
ReactDOM.createRoot(document.querySelector("#app")).render(<App />)
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div id="app"></div>
虽然我没有在 React 文档中看到这种明确称为反模式的情况,但如上所示,它有可能隐藏程序中的一些坏错误。因此,我认为您应该避免将组件存储为状态。
这里需要做出一个重要的区别:组件是 props、state 和 context 的函数,它返回一个element。您在该上下文中存储的内容是一个元素。 element是React指定的形状的对象(您可以尝试
console.log
来亲自查看它)。
由于您可以在状态和上下文中存储函数和对象,因此您也可以在那里完全存储组件和元素。另外,您可以将它们作为道具传递。
给定一个状态:
const [sidebarContent, setSidebarContent] = useState<null | React.ReactNode>(
null,
);
还有一个组件
function MyComponent(props) {
console.log('MyComponent props', props)
return (
<div>MyComponent</div>
);
}
在状态中存储组件:
setSidebarContent(MyComponent);
//usage:
<Sidebar>
<MyComponent prop1={someProp1} prop2={someProp2}/>
</Sidebar>
在状态中存储元素(就像你所做的那样):
//somewhere in a handler or in an effect
setSidebarContent(<MyComponent prop1={someProp1} prop2={someProp2}/>);
//usage:
<Sidebar>
{sidebarContent}
</Sidebar>
作为道具传递:
<Sidebar
contentAsComponent={MyComponent}
contentAsElement={<MyComponent prop1={someProp1} prop2={someProp2}/>}
/>
主要区别在于设置道具的位置。