What is the preferred way to change state in React using Hooks?
Option 1: I think this is the more "traditional"
function App() {
const [input, setInput] = useState('');
const onInputChange = (event) => {
setInput(event.target.value)
};
return (
<div className='App'>
<Form onInputChange={onInputChange} />
</div>
);
}
const Form = ({ onInputChange }) => {
return (
<div>
<div>
<div>
<input
type='text'
onChange={onInputChange}
/>
<button'>
Submit
</button>
</div>
</div>
</div>
);
};
export default Form;
Option 2: But I have seen people do this
function App() {
const [input, setInput] = useState('');
return (
<div className='App'>
<Form onInputChange={setInput} />
</div>
);
}
export default App;
const Form = ({ onInputChange }) => {
return (
<div>
<div>
<div>
<input
type='text'
onChange={(e) => {
onInputChange(e.target.value);
}}
/>
<button>
Submit
</button>
</div>
</div>
</div>
);
};
export default Form;
In Option 1 in the App component the logic to change it's state is there.
In Option 2, the logic to change App component is in a child component, right?
If I understand correctly, I think Option 1 makes more sense. What do you think?
option 1 is better because we only have to pass the props or data to child component not the logic , Logic must be reside in parent component.
Related
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'm having trouble passing data between two React siblings. I need to have an input component that will receive and pass the input to sibling that will handle the input and perform some logic.
I get to receive the data in the parent, but I cannot seem to get the passing to the sibling to work...
This is the wrapper/parent
const [corpBrandId, setCorpBrandId] = useState("");
const [secondInput, setSecondInput] = useState("");
const handleCorpBrandIdChange = ({ target }) => {
setCorpBrandId(target.value);
};
const handleSecondInputChange = ({ target }) => {
setSecondInput(target.value);
};
const handleClick = () => {
// console.log(corpBrandId);
// console.log(secondInput);
};
return (
<div className="App">
<InputField
label="Corporate brand ID "
onChange={handleCorpBrandIdChange}
value={corpBrandId}
/>
<TableConnect corpBrandId={corpBrandId} secondInput={secondInput} />
<InputField
label="Second input param"
onChange={handleSecondInputChange}
value={secondInput}
/>
<button onClick={handleClick}>Click me</button>
</div>
);
}
export default MainWrapper;
And here's the input component:
return (
<div>
<label>{label}</label>
<input type="text" value={value} name={name} onChange={onChange} />
</div>
);
};
export default InputField;
I've got no working or remotely close code how to receive the data...
This doesn't work in the sibling:
return (
<div>
Test
{props.corpBrandId}
{props.secondInput}
</div>
);
}
Anyone got any idea?
Based on your comment to #Shahar's answer, it sounds like you maybe haven't defined props in your TableConnect component. It should look something like this:
const TableConnect = (props) => {
return (
<div>
Test
{props.corpBrandId}
{props.secondInput}
</div>
);
};
Here's a codesandbox where TableConnect is correctly receiving the input from your input fields: https://codesandbox.io/s/throbbing-cloud-wgub8?file=/src/App.js
export default InputField;
return (
<div>
Test
{props.corpBrandInput} //shouldnt it be: corpBrandId
{props.inputText} //shouldnt it be: secondInput
</div>
);
}
I have a parent component that has a button with an onClick event and when there is an error I want to focus on an input that is in a child component. I know this can be done using useRef but I keep getting an undefined error. Here is my code:
/* Parent Component */
const parent = () => {
const acresRef = useRef();
const addrRef = useRef();
const acresFocus = () => {
acresRef.current.focus();
};
const addressFocus = () => {
addrRef.current.focus();
};
return (
<Child addrRef={addrRef} acresRef={acresRef} />
<button onClick={acresFocus} />
<button onCLick={addressFocus} />
)
}
/*Child Component*/
const Child = forwardRef(
({props}, acresRef, addrRef) => (
<div>
<label for="address">Address</label>
<input type="text" name="address" ref={addrRef} />
</div>
<div>
<label for="acres">Acres</label>
<input type="text" name="acres" ref={acresRef} />
</div>
)
);
You are incorrectly using refs here. forwardRef works when ref is passed to the component. Anything apart from that is a prop to the component. So in your case, acresRef and addrRef will be received within props by the child component.
/*Child Component*/
const Child = (props) => (
<>
<div>
<label htmlFor="address">Address</label>
<input type="text" name="address" ref={props.addrRef} />
</div>
<div>
<label htmlFor="acres">Acres</label>
<input type="text" name="acres" ref={props.acresRef} />
</div>
</>
);
/* Parent Component */
const Parent = () => {
const acresRef = React.useRef();
const addrRef = React.useRef();
const acresFocus = () => {
acresRef.current.focus();
};
const addressFocus = () => {
addrRef.current.focus();
};
return (
<>
<Child addrRef={addrRef} acresRef={acresRef} />
<button onClick={acresFocus}>acres</button>
<button onClick={addressFocus}>address</button>
</>
);
};
Check this codesandbox here.
You may also just bind the props like so :
const Child = (props) => {
const onClick = props.onClick;
return(<div><p>
{onClick()}
</p>
</div>
);
}
/* Parent Component */
const ParentEl = () => {
const onClick = ()=>{return "hi"};
return (
<div>
<Child onClick={onClick} />
</div>
)
}
/*Child Component*/
export default function App() {
return (
<div className="App">
<ParentEl />
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
or a codesandbox link to see directly what it does :
https://codesandbox.io/s/hidden-water-s8qjj?file=/src/App.js
here :
const onClick = props.onClick;
in the child component is doing all the magic
I tried to put a form in a separate reusable component but when used that way I can't type anything into the input. I observed, that after entering one letter (it does not appear in the input box) it seems that React rerender the whole component and the name is updated with the inserted letter.
in the version 2 the same code works correctly.
// the part same for the both versions
const [userdata, setUser] = useState({});
const { name } = userdata
const handleChange = key => event => {
setUser({
...userdata,
[ key ]: event.target.value
});
};
const submitEdit = event => {
event.preventDefault();
handleChange();
};
// VERSION 1. doesn't work
const FormEdit = () => (
<form>
<div className="form-group">
<input onChange={handleChange("name")} type="text"/>
</div>
<button onClick={submitEdit}> Submit </button>
</form>
)
return (
<Layout>
<div>
{name} //<-it shows only one letter
<FormEdit />
</div>
</Layout>
);
// VERSION 2 -> works properly
return (
<Layout>
<div>
{name} //<-the updated name is shown immediately
<form>
<div className="form-group">
<input onChange={handleChange("name")} type="text"/>
</div>
<button onClick={submitEdit}> Submit </button>
</form>
</div>
</Layout>
);
};
export default User;
The issue is directly related to declaring the FormEdit component within the other component. Here's why:
In a functional component, everything declared inside gets destroyed and re-created each render. It's no different than a normal function call. This is what makes React's hooks so special. They keep track of values in between renders and make sure they are re-created with the correct values.
You're declaring the FormEdit component inside a function, which means not only is it re-declared every render, but as a side-effect it also un-mounts and remounts each render as well.
This has a few different effects:
The component's input loses focus every render.
It's impossible for it to maintain its own state.
It's not very performant.
Below is a working example to demonstrate.
const {useState, useEffect} = React;
const Example = () => {
// the part same for the both versions
const [userdata, setUser] = useState({});
const { name } = userdata
const handleChange = (key) => (event) => {
setUser({
...userdata,
[ key ]: event.target.value
});
};
const submitEdit = (event) => {
event.preventDefault();
handleChange();
};
const FormEdit = () => {
useEffect(() => {
console.log('mount');
return () => console.log('unmount');
}, []);
return (
<form>
<div>
<input onChange={handleChange("name")} type="text"/>
</div>
<button onClick={submitEdit}> Submit </button>
</form>
)
}
return (
<div>
{name}
<FormEdit />
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
As for why you only see the first character; You are not giving the input a value, only an onChange. If the component does not unmount, this just makes it an "uncontrolled" component. The input still gets it's value updated, you just can't programatically control it. But, since it is unmounting and re-mounting every render, it loses its last value every time the user types.
Making it a controlled input would fix this:
const {useState, useEffect} = React;
const Example = () => {
// the part same for the both versions
const [userdata, setUser] = useState({});
const { name } = userdata
const handleChange = (key) => (event) => {
setUser({
...userdata,
[ key ]: event.target.value
});
};
const submitEdit = (event) => {
event.preventDefault();
handleChange();
};
const FormEdit = () => {
useEffect(() => {
console.log('mount');
return () => console.log('unmount');
}, []);
return (
<form>
<div>
<input value={name} onChange={handleChange("name")} type="text"/>
// ^ Add this
</div>
<button onClick={submitEdit}> Submit </button>
</form>
)
}
return (
<div>
{name}
<FormEdit />
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
This is a little better, but still not ideal. Now it keeps the value each update, but it still loses focus. Not a very good user experience.
This final solution is to never declare a component within another component.
const {useState, useEffect} = React;
const FormEdit = (props) => {
useEffect(() => {
console.log('mount');
return () => console.log('unmount');
}, []);
return (
<form>
<div>
<input value={props.name} onChange={props.handleChange("name")} type="text"/>
</div>
<button onClick={props.submitEdit}> Submit </button>
</form>
)
}
const Example = () => {
// the part same for the both versions
const [userdata, setUser] = useState({});
const { name } = userdata
const handleChange = (key) => (event) => {
setUser({
...userdata,
[ key ]: event.target.value
});
};
const submitEdit = (event) => {
event.preventDefault();
handleChange();
};
return (
<div>
{name}
<FormEdit name={name} handleChange={handleChange} submitEdit={submitEdit} />
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Now it only mounts once, keeps focus, and updates as expected.
You would have to pass your form handlers to the child component as props so that the lifted state can be manipulated from the child.
// Parent Component
...
const [userdata, setUser] = useState({});
const { name } = userdata
const handleChange = key => event => {
...
};
const submitEdit = event => {
...
};
return (
<Layout>
<div>
{name}
<FormEdit handleChange={handleChange} submitEdit={submitEdit}/>
</div>
</Layout>
);
and then in the child:
// Child Component
const FormEdit = (props) => (
<form>
<div className="form-group">
<input onChange={props.handleChange("name")} type="text"/>
</div>
<button onClick={props.submitEdit}> Submit </button>
</form>
)
Your FormEdit component which is inside the App component is causing the entire App component to re-render when the state gets updated onChange and hence you can only enter only one character at a time. It is generally not a great idea to declare a component within a component. Refer this link for more info. All you have to do is pull the FormEdit component out of the App component in its own separate function and pass the change handlers as props to the FormEdit component. Have a look at the working code below.
import React, { useState } from 'react';
const FormEdit = ({ handleChange, submitEdit, name }) => {
return (
<form>
<div className='form-group'>
<input onChange={handleChange('name')} type='text' value={name || ''} />
</div>
<button onClick={submitEdit} type='submit'>
Submit
</button>
</form>
);
};
export default function App() {
const [userdata, setUser] = useState();
const { name } = userdata || {};
const handleChange = key => event => {
setUser(prevState => {
return { ...prevState, [key]: event.target.value };
});
event.persist();
event.preventDefault();
};
const submitEdit = event => {
event.preventDefault();
handleChange();
};
return (
<div>
<div>
{name || ''}
<FormEdit
handleChange={handleChange}
submitEdit={submitEdit}
name={name}
/>
</div>
</div>
);
}
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