In my case I use Redux and want to make a reusable component that can pass from parent to child component when onChange function is being called. But not sure how to do that in dumb component, so I want to have an input field that when a user type some letters/numbers in that field. It should call the parent onChange function and based on that it should find the right component and pass that value to that component. Allmost any components except the CheckBox do have an onChange function. Is there a way to write that DRY and in a reusable way. There are more codes in the snippets below, please scroll down more.
const Parent = () => {
const checkForm = () => {
console.log('checkForm');
}
const onChange = (e) => {
getValue(e.target.value, e.target.name);
}
return (
<div className="App">
<form onSubmit={checkForm}>
<Name onChange={onChange}/>
<Surname onChange={onChange}/>
<Email onChange={onChange}/>
<PromoCode onChange={onChange}/>
<CheckBox/>
<button type="submit">Submit</button>
</form>
</div>
);
}
const mapDispatchToProps = dispatch => ({
getValue: (inputValue, componentName) => dispatch(getValue(inputValue, componentName))
});
const Name = () => {
return (
<div>
Name
<input
name='name'
type="text"
// value={this.state.name}
// onBlur={nameOnBlur}
onChange={onChange}
placeholder="Enter first name"
/>
</div>
)
}
const mapDispatchToProps = dispatch => ({
getValue: (inputValue, componentName) => dispatch(getValue(inputValue, componentName))
});
const mapStateToProps = (state) => {
return {
curInputValue: state.form.name || []
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Name);
export const form = (state = initialState, action) => {
switch(action.type) {
case types.GET_VALUE: {
return {
...state,
[action.componentName]: action.value
}
}
default:
return state;
}
}
To summarize the question I believe you are asking:
How do I write an onChange handler that can handle updating a variety of textfields inputs.
This could look something like:
const [formData, setFormData] = useState({name: '', surname: '', etc.})
const handleChange = (name) => event => {
setFormData({...state, [name]: event.target.value })
}
return (
<div>
<Name value={name} onChange={handleChange('name')} />
<Surname value={surname} onChange={handleChange('surname')} />
{...content}
</div> )
The key thing to note is that we store state in an object, one event handler is called for each time and it changes based on the param used.
If you are interested in using redux with this (I would advise against that unless you absolutely have to given the complexity it adds), the code would look similar but you would instead have to dispatch for each onChange fired and and the value would then be something like value={props.form.name}
Related
I have the code below.
How can I reset the stocksHelper, instatiating again when the component render on stocks's useState change?
I need this class to instantiate again to reset the variables inside the class instance, because when the stocks change a calculation needs to be done to render the stocks again. And if I get the instance of the last render with the old values this calculation will bug my entire aplication
export default function Heatmap() {
const [stocks, setStocks] = useState<IStock[]>([]);
const stocksHelper: StocksHelper = new StocksHelper(stocks);
return (
<main className={styles.main}>
<RegisterForm stocks={stocks} setStocks={setStocks} />
</main>
);
}
RegisterForm component below:
export default function RegisterForm(props: Props) {
const { stocks, setStocks } = props;
const [name, setName] = useState<string>('');
const [value, setValue] = useState<number>(0);
const [volume, setVolume] = useState<number>(0);
function storeStock(): void {
axios.post('any url', {
name: name,
value: value,
volume: volume
})
.then((res) => {
setStocks([...stocks, res.data]);
})
.catch((res) => console.log(res));
}
return (
<form className={styles.form} onSubmit={() => storeStock()}>
<fieldset>
<legend className={styles.title}>Cadastro</legend>
<input type="text" onChange={e => setName(e.target.value)} placeholder="Nome" />
<input type="number" onChange={e => setValue(parseFloat(e.target.value))} placeholder="Porcentagem" />
<input type="number" onChange={e => setVolume(parseInt(e.target.value))} placeholder="Volume" />
<button type='submit'>Cadastrar</button>
</fieldset>
</form>
);
}
#AmitMaraj's answer is perfectly fine but for a shorter and more concise method you should use useMemo:
const stocksHelper = useMemo(() => new StocksHelper(stocks), [stocks]);
Now a new StocksHelper will only be created when stocks changes.
Pass a “create” function and an array of dependencies. useMemo will only recompute the memoized value when one of the dependencies has changed. This optimization helps to avoid expensive calculations on every render.
Link to documentation
If I'm understanding correctly, you might be able to achieve this with useEffect! See below for an example:
export default function Heatmap() {
const [stocks, setStocks] = useState<IStock[]>([]);
const [stocksHelper, setStockHelper] = useState<StocksHelper>(new StocksHelper(stocks));
useEffect(() => {
setStockHelper(new StocksHelper(stocks))
}, [stocks])
return (
<main className={styles.main}>
<RegisterForm stocks={stocks} setStocks={setStocks} />
</main>
);
}
I have a webpage with multiple forms. Here's a bare minimum example of the structure:
export default function Example() {
const [previousFormFetchedFromWeb, setPreviousFormFetchedFromWeb] = useState(
{}
);
const [formA, setFormA] = useState({});
const [formB, setFormB] = useState({});
const router = useRouter();
useEffect(() => {
fetchFormFromWeb(router.query.id).then((previousForm) => {
console.log("fetched info from web");
setPreviousFormFetchedFromWeb(previousForm);
setFormA(previousForm.formA);
setFormB(previousForm.formB);
});
},[router.isReady]);
return (
<>
<FormA form={formA} />
<FormB form={formB} />
</>
);
}
function FormA({ form }) {
return (
<input
type="text"
name="field1"
id="field1"
value={formA.field1}
onChange={(e) => {
setFormA(e.target.value);
}}
/>
);
}
function FormB({ form }) {
return (
<input
type="text"
name="field2"
id="field2"
value={formB.field2}
onChange={(e) => {
setFormB(e.target.value);
}}
/>
);
}
I think this should be the encapsulated logic of my form page. The problem is that when the onChange event is called for a field of any form, all forms get re-rendered. I assumed that setState should re-render only the components with the affected dependency change. Am I missing something?
Any state change in Example component will trigger re render to its child components (FormA, FormB). If you want to avoid that. wrap FormA and FormB in React.memo. That way you can prevent unwanted re-renders
export default function Example() {
const [previousFormFetchedFromWeb, setPreviousFormFetchedFromWeb] = useState(
{}
);
const [formA, setFormA] = useState({});
const [formB, setFormB] = useState({});
const router = useRouter();
useEffect(() => {
fetchFormFromWeb(router.query.id).then((previousForm) => {
console.log("fetched info from web");
setPreviousFormFetchedFromWeb(previousForm);
setFormA(previousForm.formA);
setFormB(previousForm.formB);
});
},[router.isReady]);
return (
<>
<MemFormA form={formA} />
<MemFormB form={formB} />
</>
);
}
const MemFormA = React.memo(function FormA({ form }) {
return (
<input
type="text"
name="field1"
id="field1"
value={formA.field1}
onChange={(e) => {
setFormA(e.target.value);
}}
/>
);
})
const MemFormB = React.memo(function FormB({ form }) {
return (
<input
type="text"
name="field2"
id="field2"
value={formB.field2}
onChange={(e) => {
setFormB(e.target.value);
}}
/>
);
})
Anytime state of <Example /> is updated, <Example /> re-renders, which in turn also re-renders <FormA /> and <FormB />. This is expected.
You should look into using React.memo() for FormA and FormB if you want them to only re-render when the props passed to them is changed.
I want to create an Input component to be used to compose new form elements.
const Input = ({ value, children }) => {
const [currentValue, setCurrentValue] = useState();
return <div className='input'>
{children}
</div>
};
And my Text component would be:
const Text = (props) => {
return <Input {...props}>
<input
type='text'
value={/*how to bind to currentValue of Input*/}
onChange={/*how to call Input's setCurrentValue here*/}
/>
</Input>
}
I need to store currentValue state in parent, because I need to manage it for many different inputs.
Also I'm stuck at how to call parent's setCurrentValue on child's onChange method.
Any help is appreciated.
Update:
CodeSandbox
Update 2:
Real code from my repository
Solutions:
Context API
Pass props to children
Use children as funciton & pass relevant
Send that reference somehow using any method you see fit.
My preference: Composition with function
const Input = ({ value, children }) => {
const [currentValue, setCurrentValue] = useState();
const handlChange = (e) => {
setCurrentValue(e.target.value);
};
return <div className='input'>
{children(handlChange)}
</div>
};
const Text = (props) => {
return <Input {...props}>
{ (handleChange) => (
<input
type='text'
onChange = ( handleChange }
/>
) }
</Input>
}
Explanations:
How to pass props to {this.props.children}
https://victorofoegbu.com/notes/pass-props-to-react-children-faq
Please try like this.
// pass props to children.
const Input = ({ value, children }) => {
const [currentValue, setCurrentValue] = useState();
return <div className='input'>
{React.cloneElement(child, {onChange: setCurrentValue, value: currentValue}))}
</div>
};
// using props in childern.
const Text = (props) => {
return <Input {...props}>
{
({onChange, value})=> (
<input
type='text'
value={value}
onChange={(e)=>onChange(e.target.value)}
/>
)
}
</Input>
}
I have a simple react app in which there is a FruitsList component for showing the fruits in the list, a FruitForm component to add a fruit, and both are contained inside a Fruits component. I am using useContext and useReducer to manage the state of the fruits. I have created a FruitContext for same. I want to stop the re-rendering of FruitForm since it is only using dispatch function and re-rendering it is useless every time new fruit is added. Plz suggest any solution for same.
Form Component
const Form = () => {
const { dispatch } = useContext(FruitsContext);
const { setLoading } = useContext(LoaderContext);
let formRef = null;
const fruit = {};
const formSubmitHandler = async (event) => {
event.preventDefault();
setLoading(true);
await fetch('https://fruit-basket-74269.firebaseio.com/fruits.json', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(fruit)
});
dispatch({type: 'ADD', fruit: fruit});
// formRef.reset();
setLoading(false);
}
return (
<Card>
{console.log('[Form]')}
<form ref={ref => formRef = ref} onSubmit={formSubmitHandler} className={style.form} autoComplete="off">
<div className={style.formGroup}>
<input onChange={event => fruit.item = event.target.value} className={style.input} type="text" id="name" placeholder="Enter fruit name" />
<label className={style.label} htmlFor="name">Name</label>
</div>
<div className={style.formGroup}>
<input onChange={event => fruit.qty = event.target.value} className={style.input} type="number" min="0" id="qty" placeholder="Enter quantity" />
<label className={style.label} htmlFor="qty">Quantity</label>
</div>
<Button>Add Fruit</Button>
</form>
</Card>
)
}
export default React.memo(Form);
FruitList
const FruitList = () => {
const { fruits } = useContext(FruitsContext);
console.log('[FruitList]:', fruits);
return useMemo(() => {
return (
<div className={style.fruitList}>
<h2 className={style.heading}>Fruits</h2>
<hr />
<div className={style.list}>
<FruitCard name={'Apple'} qty={15} />
<FruitCard name={'Orange'} qty={10} />
<FruitCard name={'Grapes'} qty={20} />
</div>
</div>
);
}, []);
}
export default FruitList;
Fruits
const Fruits = () => {
console.log('[Fruits Parent]');
// const { loading } = useContext(LoaderContext);
return (
<div className={style.fruits}>
{/* {loading && <Loader />} */}
<Form />
<br />
<Filter />
<br />
<FruitList />
</div>
)
}
export default Fruits
FruitContext
export const FruitsContext = createContext();
const FruitsProvider = ({children}) => {
const [fruits, dispatch] = useReducer(reducer, []);
const value = ({
fruits, dispatch
});
return (
<FruitsContext.Provider value={value}>
{ children }
</FruitsContext.Provider>
);
}
export default FruitsProvider;
FruitReducer
export default (state, action) => {
switch(action.type) {
case 'LOAD':
return action.fruits
case 'ADD':
console.log('[Pre-Action]', state);
const newList = [...state];
newList.push(action.fruit);
console.log('[Post-Action]', newList);
return newList;
case 'DELETE':
return state.filter(fruit => fruit.id !== action.id);
default: return state;
}
}
Components that consume a context will always rerender if anything in the providers value changed. Regardless of whether you actually use that value or not (In this case for example even if you only pull the dispatch function).
Usually you don't need to optimize something like that for most react applications, react is already quite fast and a few additional rerenders don't hurt. Any performance issues can be solved when and where they happen.
If you want to optimize from the start you can split your reducers state and dispatch into two different contexts. They both can be put into the same ProviderComponent but there have to be two different Context.Provider components. One will use the state as value and the other will use the dispatch function as value.
If you then consume the dispatch context, it won't cause the component to rerender if the dispatch action changes the state.
// Update
As an example:
const FruitsProvider = ({children}) => {
const [fruits, dispatch] = useReducer(reducer, []);
return (
<FruitsStateContext.Provider value={fruits}>
<FruitsDispatchContext.Provider value={dispatch}>
{ children }
</FruitsDispatchContext.Provider>
</FruitsStateContext.Provider>
);
}
I would also recommend to not export the context directly but instead export hooks that expose the state or the dispatch.
e.g.
export const useFruits = () => {
const fruitsState = React.useContext(FruitsStateContext);
if (!fruitsState) {
throw new Error('you cant use the useFruits hook outside the FruitsStateContext');
}
return fruitsState;
}
I'm not yet a React master, hence my question. Why there is still invoking a parent function if in child component I'm writing new characters in input fields? I want to call parent method only when I clicked Search button in my child component.
Parent component:
class MainPage extends Component {
render() {
let searchOffersBar = (
<MuiThemeProvider>
<SearchOffer
offersFound={this.props.onOffersFound}
/>
</MuiThemeProvider>
);
let searchResults = (
<SearchResults
offers={this.props.offers}
/>
);
return (
<Aux>
<div className={classes.container}>
<Intro/>
<div className={classes.contentSection}>
{searchOffersBar}
{searchResults}
</div>
</div>
</Aux>
)
}
}
const mapStateToProps = state => {
return {
offers: state.offers.offers
}
}
const mapDispatchToProps = dispatch => {
return {
onOffersFound: (searchParams) => dispatch(actions.fetchOffersByCriteria(searchParams))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(MainPage);
<SearchOffer> is my child component with a search section (input fields and button "Search offers"). I want to fill some data in my inputs and then click the button. I though that clicking the button will invoke a method in child component: onOffersFound:
const searchOffer = props => {
let currentDate = new Date();
const [searchCriteria, setSearchCriteria] = useState({
brand: 'xxx',
capacity: 100
})
const [drawerIsOpen, setDrawerIsOpen] = useState(false);
const handleToggle = () => setDrawerIsOpen(!drawerIsOpen);
const handleBrand = (event) => {
let mergedState = updateObject(searchCriteria, {brand: event.target.value})
setSearchCriteria(mergedState);
}
const handleCapacity = (event) => {
let mergedState = updateObject(searchCriteria, {capacity: event.target.value});
setSearchCriteria(mergedState);
}
const handleBookingFrom = (bookingFromValue) => {
let mergedState = updateObject(searchCriteria, {bookingFrom: bookingFromValue});
setSearchCriteria(mergedState);
}
const handleBookingTo = (bookingToValue) => {
let mergedState = updateObject(searchCriteria, {bookingTo: bookingToValue});
setSearchCriteria(mergedState);
}
return (
<div className={classes.sideNav}>
<Button variant={"outlined"} onClick={handleToggle} className={classes.sideNavBtn}>Search</Button>
<Drawer
className={classes.drawer}
containerStyle={{top: 55}}
docked={false}
width={200}
open={drawerIsOpen}
onRequestChange={handleToggle}
>
<AppBar title="Search"/>
<form noValidate autoComplete="off" onSubmit={props.offersFound(searchCriteria)}>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<Grid container justify="space-around">
<TextField
id="brand"
label="Brand"
margin="normal"
onChange={handleBrand}
/>
<TextField
id="capacity"
label="Capacity"
margin="normal"
onChange={handleCapacity}
/>
<Button variant="contained" color="primary">
Search
</Button>
</Grid>
</MuiPickersUtilsProvider>
</form>
</Drawer>
</div>
);
}
export default searchOffer;
onOffersFound in my action creator looks like:
export const fetchOffersByCriteria = (searchParams) => {
return dispatch => {
let queryParams = '?brand='+searchParams.brand + '&capacity='+searchParams.capacity;
axios.get('/getFilteredOffers' + queryParams)
.then(response => {
dispatch(saveFoundOffers(response.data)); --> saves the state
})
.catch(error => {
console.log(error);
})
}
}
My question is why the above method fetchOffersByCriteria is invoked every time I enter new character in my child component? I want to invoke this method only when I click the Search button in child component. Maybe my approach is bad?
Thanks for all tips!
The issue is that props.offersFound(searchCriteria) is being invoked every render. The onSubmit prop should be a function to be invoked when submitted. Currently, it's being invoked immediately.
This line:
onSubmit={props.offersFound(searchCriteria)}
Should be (or something similar):
onSubmit={() => props.offersFound(searchCriteria)}
Currently, when typing in the brand (or capacity) field, the handleBrand change callback is invoked. This invokes setSearchCriteria (a state update) which triggers a re-render of the component. While this component is re-rendering, it's immediately invoking props.offersFound(searchCriteria) and passing the return value to the onSubmit prop. You likely want the onSubmit prop to be a function to be invoked at the time of submitting.
See the documentation for controlled components for more de3tails.
<form
noValidate
autoComplete="off"
onSubmit={props.offersFound(searchCriteria)}>
You are immediately invoking prop and trying to use result returned as event listener. It should be
<form
noValidate
autoComplete="off"
onSubmit={() => props.offersFound(searchCriteria)}>
instead