I was looking at the react docs today (https://beta.reactjs.org/learn/preserving-and-resetting-state), and I'm a bit confused on something.
In the docs, it says that you can force the state to reset by rendering a component in different positions, I get that.
When isPlayerA state changes, this one does not reset the state for Counter:
// App.js
import { useState } from 'react';
export default function Scoreboard() {
const [isPlayerA, setIsPlayerA] = useState(true);
return (
<div>
{isPlayerA ? (
<Counter person="Taylor" />
) : (
<Counter person="Sarah" />
)}
<button onClick={() => {
setIsPlayerA(!isPlayerA);
}}>
Next player!
</button>
</div>
);
}
function Counter({ person }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{person}'s score: {score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
When isPlayerA state changes, this one DOES reset the state for Counter:
return (
<div>
{isPlayerA &&
<Counter person="Taylor" />
}
{!isPlayerA &&
<Counter person="Sarah" />
}
<button onClick={() => {
setIsPlayerA(!isPlayerA);
}}>
Next player!
</button>
</div>
);
In both situations, it seems to be that Counter is getting rendered in the same position, so why is the second one resetting the state? Are there two Counter components in the UI tree on the second one where state does get reset?
Thanks for any help!
Related
How to pass active state, when a button is clicked in below React component?
I have a component:
<MyLinks links={links}/>
Where I pass this array: const links: CopyType[] = ['link', 'embed'];
// MyLinks component:
const [copyLinkType, setCopyLinkType] = useState<CopyType>();
return (
<React.Fragment>
<ul>
{links.map((linkType) => (
<TabIcon
type={linkType}
onClick={() => {
setCopyLinkType(linkType);
}}
/>
))}
</ul>
{copyLinkType && <TabPanel type={copyLinkType} />}
</React.Fragment>
);
In the frontend you get 2 buttons which show/hide the <TabPanel /> with it's associated content.
How to get/pass an active state when a button is clicked?
I tried passing isActive state as a prop through <TabIcon isActive={isActive} /> and then on the onClick handler setIsActive(true); but this will pass active state to both buttons in the same time?
Try to use refs. Something like this (w/o typescript codepen):
const TabIcon = (props) => {
const {activeRefs, onClick, type, index} = props
return (
<a onClick={()=>{
// On each click change value to the opposite. Or add your
// logic here
activeRefs.current[index] = !activeRefs.current[index]
onClick()
}}>
{type}
</a>
)
}
const MyLinks = (props) => {
const [copyLinkType, setCopyLinkType] = React.useState();
// Array which holds clicked link state like activeRefs.current[index]
const activeRefs = React.useRef([]);
const links = ['link1', 'link2'];
console.log({activeRefs})
return (
<ul>
{links.map((linkType, index) => (
<TabIcon
index={index}
activeRefs={activeRefs}
type={linkType}
onClick={() => {
setCopyLinkType(linkType);
}}
/>
))}
</ul>
);
}
ReactDOM.render(
<MyLinks />,
document.getElementById('root')
);
I am using react instant search library and my issue is that my custom refinement list components loses its selections when I open modal.
I control my modal with useState:
const [modalIsOpen, setModalIsOpen] = useState(false);
Everytime I call setModalIsOpen(true); the refinements reset.
My custom refinement list component:
const RefinementList = ({ items, refine }: RefinementListProvided) => {
// return the DOM output
return (
<div className="">
{items.map(({ value, label, count, isRefined }: any) => (
<div key={value}>
<motion.button
onClick={() => {
refine(value);
}}
className={``}
>
<div className="">
{label}
</div>
</motion.button>
</div>
))}
</div>
);
};
I connect it with connectRefinementList
const CustomRefinementList = connectRefinementList(RefinementList);
This is my main jsx:
<InstantSearch searchClient={searchClient} indexName="foods">
<CustomSearchBox />
<CustomRefinementList
transformItems={(items) => orderBy(items, "label", "asc")} // this prevents facets jumping
attribute="tags"
/>
<InfiniteHits hitComponent={Hit} cache={sessionStorageCache} />
<ModalForMealPreview
handleOpen={modalIsOpen}
handleClose={handleModalClose}
/>
</InstantSearch>
What can I do to persist state or prevent RefinementList component from rerendering?
Here is a basic Example of React.memo, this will help your code
import React, { useEffect, useState } from "react";
const MemoComp = React.memo(({ ...props }) => <Test {...props} />); // Main Area to watch
function ClassSearch() {
const [state, setState] = useState(1);
return (
<div>
<button onClick={() => setState(state + 1)}>Increase</button> <br />
<MemoComp data="memorized" /> <br />
<Test data="original" /> <br />
</div>
);
}
export default ClassSearch;
const Test = ({ data }) => {
const date = new Date().getTime();
return (
<>
Test {date} {data}
</>
);
};
I have a Parent component where is open/close logic for Child component.
Child component has open/close logic too.
But behavior of these components are incorrect, when I close child in child I can not open it again in parent. How to rewrite it by right way, may be with one whole useState?
export const Parent = () => {
const [isChildVisible, setChildVisible] = useState(false);
return (
<>
<span
className="link"
onClick={() => {
setChildVisible(!isChildVisible);
}}
>
Click
</span>
{isChildVisible && <Child />}
</>
);
};
export const Child = (props) => {
const [isClosePopup, setIsClosePopup] = useState(false);
return (
<>
{!isClosePopup && (
<StyledChild>
<span
onClick={() => {
setIsClosePopup(!isClosePopup);
}}
>
X
</span>
Content
</StyledChild>
)}
</>
);
};```
move the child states to parent and pass through the props.
export const Parent = () => {
const [isChildVisible, setChildVisible] = useState(false);
const [isClosePopup, setIsClosePopup] = useState(false);
return (
<>
<span
className="link"
onClick={() => {
setChildVisible(!isChildVisible);
}}
>
Click
</span>
{
isChildVisible && <Child isClosePopup={isClosePopup}
setIsClosePopup={setIsClosePopup}/>
}
</>
);
};
export const Child = (props) => {
const {isClosePopup,setIsClosePopup} =props
return (
<>
{!isClosePopup && (
<StyledChild>
<span
onClick={() => {
setIsClosePopup(!isClosePopup);
}}
>
X
</span>
Content
</StyledChild>
)}
</>
);
};```
I was trying to make a simple example illustrating how useContext work.
I have started with this sandbox code:
https://codesandbox.io/s/react-typescript-usecontext-lama-1v7wd
The issue is that the component Counter does not update and rerender when I click the button.
My index
import React, { useContext } from 'react'
import { MyContextProvider, MyContext } from './Context'
import { render } from 'react-dom'
const MyCounter = () => {
const context = useContext(MyContext)
const { counter } = context
const { setCounter } = context
return (
<div>
Valeur du compteur : {counter}
<br />
<button onClick={() => setCounter(counter - 1)} type="button">
-1
</button>
<button onClick={() => setCounter(counter + 1)} type="button">
+1
</button>
<br />
<button onClick={() => setCounter(0)} type="button">
RàZ
</button>
</div>
)
}
const rootElement = document.getElementById('root')
render(
<MyContextProvider>
<MyCounter />
</MyContextProvider>,
rootElement
)
My context:
type MyContextProps = {
counter: number
setCounter: Dispatch<SetStateAction<number>>
}
const MyContext = createContext({} as MyContextProps)
const MyContextProvider: React.FunctionComponent = (props) => {
const [counter, setCounter] = useState<number>(0)
return (
<MyContext.Provider
value={{
counter: 0,
setCounter: setCounter,
}}
>
{props.children}
</MyContext.Provider>
)
}
export { MyContext, MyContextProvider }
It's got to be something elementary, but I just can't see it.
Just a small error.
in your context, you have set your counter to be always zero. Change this to be counter state and your problem should be resolved.
const MyContextProvider: React.FunctionComponent = (props) => {
const [counter, setCounter] = useState<number>(0)
return (
<MyContext.Provider
value={{
counter: counter, //<-- HERE.
setCounter: setCounter,
}}
>
{props.children}
</MyContext.Provider>
)
}
As a suggestion, consider using function to get the latest state of your value when setting the next value using the setCounter function, if the next value is dependent on the previous value.
The mutation function from useState can also accept a callback function, which provides the latest current state and should return the next state value based on the previous value. This is especially helpful if setting state is in an async operation preventing stale closures.
(prevValue) => nextValue
<button onClick={() => setCounter(prevValue => prevValue - 1)} type="button">
and
<button onClick={() => setCounter(prevValue => prevValue + 1)} type="button">
I have a form page structured more or less as follows:
<Layout>
<Page>
<Content>
<Input />
<Map />
</Content>
</Page>
<Button />
</Layout>
The Map component should only be rendered once, as there is an animation that is triggered on render. That means that Content, Page and Layout should not re-render at all.
The Button inside Layout should be disabled when the Input is empty. The value of the Input is not controlled by Content, as a state change would cause a re-render of the Map.
I've tried a few different things (using refs, useImperativeHandle, etc) but none of the solutions feel very clean to me. What's the best way to go about connecting the state of the Input to the state of the Button, without changing the state of Layout, Page or Content? Keep in mind that this is a fairly small project and the codebase uses "modern" React practices (e.g. hooks), and doesn't have global state management like Redux, MobX, etc.
Here is an example (click here to play with it) that avoids re-render of Map. However, it re-renders other components because I pass children around. But if map is the heaviest, that should do the trick. To avoid rendering of other components you need to get rid of children prop but that most probably means you will need redux. You can also try to use context but I never worked with it so idk how it would affect rendering in general
import React, { useState, useRef, memo } from "react";
import "./styles.css";
const GenericComponent = memo(
({ name = "GenericComponent", className, children }) => {
const counter = useRef(0);
counter.current += 1;
return (
<div className={"GenericComponent " + className}>
<div className="Counter">
{name} rendered {counter.current} times
</div>
{children}
</div>
);
}
);
const Layout = memo(({ children }) => {
return (
<GenericComponent name="Layout" className="Layout">
{children}
</GenericComponent>
);
});
const Page = memo(({ children }) => {
return (
<GenericComponent name="Page" className="Page">
{children}
</GenericComponent>
);
});
const Content = memo(({ children }) => {
return (
<GenericComponent name="Content" className="Content">
{children}
</GenericComponent>
);
});
const Map = memo(({ children }) => {
return (
<GenericComponent name="Map" className="Map">
{children}
</GenericComponent>
);
});
const Input = ({ value, setValue }) => {
const onChange = ({ target: { value } }) => {
setValue(value);
};
return (
<input
type="text"
value={typeof value === "string" ? value : ""}
onChange={onChange}
/>
);
};
const Button = ({ disabled = false }) => {
return (
<button type="button" disabled={disabled}>
Button
</button>
);
};
export default function App() {
const [value, setValue] = useState("");
return (
<div className="App">
<h1>SO Q#60060672</h1>
<Layout>
<Page>
<Content>
<Input value={value} setValue={setValue} />
<Map />
</Content>
</Page>
<Button disabled={value === ""} />
</Layout>
</div>
);
}
Update
Below is version with context that does not re-render components except input and button:
import React, { useState, useRef, memo, useContext } from "react";
import "./styles.css";
const ValueContext = React.createContext({
value: "",
setValue: () => {}
});
const Layout = memo(() => {
const counter = useRef(0);
counter.current += 1;
return (
<div className="GenericComponent">
<div className="Counter">Layout rendered {counter.current} times</div>
<Page />
<Button />
</div>
);
});
const Page = memo(() => {
const counter = useRef(0);
counter.current += 1;
return (
<div className="GenericComponent">
<div className="Counter">Page rendered {counter.current} times</div>
<Content />
</div>
);
});
const Content = memo(() => {
const counter = useRef(0);
counter.current += 1;
return (
<div className="GenericComponent">
<div className="Counter">Content rendered {counter.current} times</div>
<Input />
<Map />
</div>
);
});
const Map = memo(() => {
const counter = useRef(0);
counter.current += 1;
return (
<div className="GenericComponent">
<div className="Counter">Map rendered {counter.current} times</div>
</div>
);
});
const Input = () => {
const { value, setValue } = useContext(ValueContext);
const onChange = ({ target: { value } }) => {
setValue(value);
};
return (
<input
type="text"
value={typeof value === "string" ? value : ""}
onChange={onChange}
/>
);
};
const Button = () => {
const { value } = useContext(ValueContext);
return (
<button type="button" disabled={value === ""}>
Button
</button>
);
};
export default function App() {
const [value, setValue] = useState("");
return (
<div className="App">
<h1>SO Q#60060672, method 2</h1>
<p>
Type something into input below to see how rendering counters{" "}
<s>update</s> stay the same
</p>
<ValueContext.Provider value={{ value, setValue }}>
<Layout />
</ValueContext.Provider>
</div>
);
}
Solutions rely on using memo to avoid rendering when parent re-renders and minimizing amount of properties passed to components. Ref's are used only for render counters
I have a sure way to solve it, but a little more complicated.
Use createContext and useContext to transfer data from layout to input. This way you can use a global state without using Redux. (redux also uses context by the way to distribute its data). Using context you can prevent property change in all the component between Layout and Imput.
I have a second easier option, but I'm not sure it works in this case. You can wrap Map to React.memo to prevent render if its property is not changed. It's quick to try and it may work.
UPDATE
I tried out React.memo on Map component. I modified Gennady's example. And it works just fine without context. You just pass the value and setValue to all component down the chain. You can pass all property easy like: <Content {...props} /> This is the easiest solution.
import React, { useState, useRef, memo } from "react";
import "./styles.css";
const Layout = props => {
const counter = useRef(0);
counter.current += 1;
return (
<div className="GenericComponent">
<div className="Counter">Layout rendered {counter.current} times</div>
<Page {...props} />
<Button {...props} />
</div>
);
};
const Page = props => {
const counter = useRef(0);
counter.current += 1;
return (
<div className="GenericComponent">
<div className="Counter">Page rendered {counter.current} times</div>
<Content {...props} />
</div>
);
};
const Content = props => {
const counter = useRef(0);
counter.current += 1;
return (
<div className="GenericComponent">
<div className="Counter">Content rendered {counter.current} times</div>
<Input {...props} />
<Map />
</div>
);
};
const Map = memo(() => {
const counter = useRef(0);
counter.current += 1;
return (
<div className="GenericComponent">
<div className="Counter">Map rendered {counter.current} times</div>
</div>
);
});
const Input = ({ value, setValue }) => {
const counter = useRef(0);
counter.current += 1;
const onChange = ({ target: { value } }) => {
setValue(value);
};
return (
<>
Input rendedred {counter.current} times{" "}
<input
type="text"
value={typeof value === "string" ? value : ""}
onChange={onChange}
/>
</>
);
};
const Button = ({ value }) => {
const counter = useRef(0);
counter.current += 1;
return (
<button type="button" disabled={value === ""}>
Button (rendered {counter.current} times)
</button>
);
};
export default function App() {
const [value, setValue] = useState("");
return (
<div className="App">
<h1>SO Q#60060672, method 2</h1>
<p>
Type something into input below to see how rendering counters{" "}
<s>update</s> stay the same, except for input and button
</p>
<Layout value={value} setValue={setValue} />
</div>
);
}
https://codesandbox.io/s/weathered-wind-wif8b