I want to store React component in React state but I think it is technically wrong. What you think about that?
Example
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>)
};
In this example with LayoutContext
I provide setter for sidebar content, and I want to know if it will cause some problem with that approach, and if there are any other approaches to set content from child components?
@Iorweth333 is technically correct, but there are serious pitfalls to watch out for -
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>
The program runs, but changing from one component state to another does not work as we might expect. React detects that state in App changed, but when {state}
is rendered, React doesn't know that is a new Counter component, as distinct from any other. So the same counter renders and the state stays the same.
If you re-key
the element containing the component stored in state, you can prompt a fresh Counter to be initialized. This is obviously a hack and should be avoided. This demo is only here to give better insight on how React "sees" things -
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>
Now that we can see with React Vision, we can understand why adding a unique key
to each Counter component would also "fix" the problem -
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>
While I haven't seen this explicitly called out as an anti-pattern in the React docs, as seen above, it has potential to hide some bad bugs in your program. For this reason, I think you should avoid storing components as state.
You asked how to make dynamic content another way, we will see that here. In React, we think about state as a snapshot, and our render is made up of props and state, conditional or derived. In this next series of code, we expand our knowledge incrementally, one demo at a time -
function App() {
const [tab, setTab] = React.useState(0) // ✅ state
return <div>
<nav>
<button
children="About"
disabled={tab == 0} // ✅ derived props
onClick={() => setTab(0)}
/>
<button
children="Contact"
disabled={tab == 1}
onClick={() => setTab(1)}
/>
</nav>
{ tab == 0 ? <About />
: tab == 1 ? <Contact /> // ✅ conditional render
: null
}
</div>
}
function About() {
return <p>About us</p>
}
function Contact() {
return <p>Contact us</p>
}
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>
Above we see how a single number stored in tab
can dynamically render content. But what if you want to supply the tabs dynamically? Here we learn how to render lists -
function App(props) {
const [tab, setTab] = React.useState(0)
const TabFn = props.tabs[tab] // ✅ get tab constructor
console.assert(TabFn, "tab out of bounds") // ✅ invariant
return <div>
<nav>
{props.tabs.map((TabFn, index) =>
<button
key={TabFn.name}
children={TabFn.name}
disabled={tab == index}
onClick={() => setTab(index)}
/>
)}
</nav>
<TabFn />
</div>
}
function Home() { return <p>Home</p> }
function About() { return <p>About us</p> }
function Contact() { return <p>Contact us</p> }
ReactDOM.createRoot(document.querySelector("#app")).render(
<App tabs={[Home, Contact, About]} /> // ✅ dynamic content
)
<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>
You might be wondering how to get data to our dynamic components. You can pass data deeply with a context. Here we begin adding a "dark mode" to our application. To keep the demo concise, only the nav
and Home
elements are responding so far -
const defaultContext = { theme: "light" } // ✅ some data
const Context = React.createContext(defaultContext) // ✅ create context
function App(props) {
const [theme, setTheme] = React.useState(defaultContext.theme)
const [tab, setTab] = React.useState(0)
const TabFn = props.tabs[tab]
console.assert(TabFn, "tab out of bounds")
return <Context.Provider value={{theme}}>
<nav className={theme}>
{props.tabs.map((TabFn, index) =>
<button key={TabFn.name} children={TabFn.name} disabled={tab == index} onClick={() => setTab(index)} />
)}
<button
onClick={() => setTheme(t => t == "light" ? "dark" : "light")}
children={`theme: ${theme}`}
/>
</nav>
<TabFn />
</Context.Provider>
}
function Home() {
const { theme } = React.useContext(Context) // ✅ access data
return <p className={theme}>Home</p>
}
function About() { return <p>About us</p> }
function Contact() { return <p>Contact us</p> }
ReactDOM.createRoot(document.querySelector("#app")).render(
<App tabs={[Home, Contact, About]} />
)
.dark {
background-color: mediumblue;
color: khaki;
}
<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>
Related: passing JSX as children to a component is normal. Note JSX can be passed in any prop, not limited to children
-
<UserProfile
avatar={<Avatar src={user.avatar} />}
name={user.name}
email={user.email}
/>