I am building an application with React where two components should be able to change state of each other:
Component A -> Component B
Component B -> Component A
The Component A is a set of buttons and the Component B is an input element.
I managed to make it work only in one way, A -> B or just B -> A, but can't make it work both. It works partly with use of useEffect hook, but with bugs and this is really stupid idea, I think.
I have read a lot that React don't work this way, but is there any roundabouts how it is possible to make it work? I really need this 2-way data binding for my application.
Thanks for any help!
The state for buttons is located in digits variable from custom context hook (useStateContext) as an array.
import { useStateContext } from "components/StateProvider/Context";
import { useState, useEffect } from "react";
import { baseConvert } from "utility/baseConvert";
const NumberInput = () => {
const [ { digits, baseIn, baseOut }, dispatch ] = useStateContext();
const convertedValue = baseConvert({
digits,
baseIn,
baseOut
});
const [ inputValue, setInputValue ] = useState(convertedValue);
/* useEffect(() => {
setInputValue(convertedValue)
}, [ digits, baseIn, baseOut ]) */
const regex = /^[A-Z\d]+$/;
const handleInput = ({ target: { value }}) => {
if (value === "") setInputValue(0);
console.log(value);
if (regex.test(value)) {
setInputValue(value);
dispatch({
type: "setDigits",
digits: baseConvert({
digits: value.split("").map(Number),
baseIn: baseOut,
baseOut: baseIn
})
})
}
};
return (
<input type="text" onChange={handleInput} value={inputValue} />
);
};
export default NumberInput;
Components should not directly manipulate the state of other components. If you need to have shared data, bring the state to a parent component and pass callbacks to the children that can change the state.
For example:
function ParentComponent() {
const [currentVal, setCurrentVal] = useState(0);
return
<>
<Child1 value={currentVal} onChange={setCurrentVal}/> // you might also pass a function that does some other logic and then calls setCurrentVal
<Child2 value={currentVal} onChange={setCurrentVal}/>
</>
}
#Jeff Storey
The solution seems a bit wrong. The first index is the value and the second index is the function which updates the value.
It should be:
const [currentVal, setCurrentVal] = useState(0);
Related
I'm working on my first React project and I have the following problem.
How I want my code to work:
I add Items into an array accessible by context (context.items)
I want to run a useEffect function in a component, where the context.items are displayed, whenever the value changes
What I tried:
Listing the context (both context and context.items) as a dependency in the useEffect
this resulted in the component not updating when the values changed
Listing the context.items.length
this resulted in the component updating when the length of the array changed however, not when the values of individual items changed.
wraping the context in Object.values(context)
result was exactly what I wanted, except React is now Complaining that *The final argument passed to useEffect changed size between renders. The order and size of this array must remain constant. *
Do you know any way to fix this React warning or a different way of running useEffect on context value changing?
Well, didn't want to add code hoping it would be some simple error on my side, but even with some answers I still wasn't able to fix this, so here it is, reduced in hope of simplifying.
Context component:
const NewOrder = createContext({
orderItems: [{
itemId: "",
name: "",
amount: 0,
more:[""]
}],
addOrderItem: (newOItem: OrderItem) => {},
removeOrderItem: (oItemId: string) => {},
removeAllOrderItems: () => {},
});
export const NewOrderProvider: React.FC = (props) => {
// state
const [orderList, setOrderList] = useState<OrderItem[]>([]);
const context = {
orderItems: orderList,
addOrderItem: addOItemHandler,
removeOrderItem: removeOItemHandler,
removeAllOrderItems: removeAllOItemsHandler,
};
// handlers
function addOItemHandler(newOItem: OrderItem) {
setOrderList((prevOrderList: OrderItem[]) => {
prevOrderList.unshift(newOItem);
return prevOrderList;
});
}
function removeOItemHandler(oItemId: string) {
setOrderList((prevOrderList: OrderItem[]) => {
const itemToDeleteIndex = prevOrderList.findIndex((item: OrderItem) => item.itemId === oItemId);
console.log(itemToDeleteIndex);
prevOrderList.splice(itemToDeleteIndex, 1);
return prevOrderList;
});
}
function removeAllOItemsHandler() {
setOrderList([]);
}
return <NewOrder.Provider value={context}>{props.children}</NewOrder.Provider>;
};
export default NewOrder;
the component (a modal actually) displaying the data:
const OrderMenu: React.FC<{ isOpen: boolean; hideModal: Function }> = (
props
) => {
const NewOrderContext = useContext(NewOrder);
useEffect(() => {
if (NewOrderContext.orderItems.length > 0) {
const oItems: JSX.Element[] = [];
NewOrderContext.orderItems.forEach((item) => {
const fullItem = {
itemId:item.itemId,
name: item.name,
amount: item.amount,
more: item.more,
};
oItems.push(
<OItem item={fullItem} editItem={() => editItem(item.itemId)} key={item.itemId} />
);
});
setContent(<div>{oItems}</div>);
} else {
exit();
}
}, [NewOrderContext.orderItems.length, props.isOpen]);
some comments to the code:
it's actually done in Type Script, that involves some extra syntax
-content (and set Content)is a state which is then part of return value so some parts can be set dynamically
-exit is a function closing the modal, also why props.is Open is included
with this .length extension the modal displays changes when i remove an item from the list, however, not when I modify it not changeing the length of the orderItems,but only values of one of the objects inside of it.
as i mentioned before, i found some answers where they say i should set the dependency like this: ...Object.values(<contextVariable>) which technically works, but results in react complaining that *The final argument passed to useEffect changed size between renders. The order and size of this array must remain constant. *
the values displayed change to correct values when i close and reopen the modal, changing props.isOpen indicating that the problem lies in the context dependency
You can start by creating your app context as below, I will be using an example of a shopping cart
import * as React from "react"
const AppContext = React.createContext({
cart:[]
});
const AppContextProvider = (props) => {
const [cart,setCart] = React.useState([])
const addCartItem = (newItem)=>{
let updatedCart = [...cart];
updatedCart.push(newItem)
setCart(updatedCart)
}
return <AppContext.Provider value={{
cart
}}>{props.children}</AppContext.Provider>;
};
const useAppContext = () => React.useContext(AppContext);
export { AppContextProvider, useAppContext };
Then you consume the app context anywhere in the app as below, whenever the length of the cart changes you be notified in the shopping cart
import * as React from "react";
import { useAppContext } from "../../context/app,context";
const ShoppingCart: React.FC = () => {
const appContext = useAppContext();
React.useEffect(() => {
console.log(appContext.cart.length);
}, [appContext.cart]);
return <div>{appContext.cart.length}</div>;
};
export default ShoppingCart;
You can try passing the context variable to useEffect dependency array and inside useEffect body perform a check to see if the value is not null for example.
While doing a code review, I came across this custom hook:
import { useRef, useEffect, useCallback } from 'react'
export default function useLastVersion (func) {
const ref = useRef()
useEffect(() => {
ref.current = func
}, [func])
return useCallback((...args) => {
return ref.current(...args)
}, [])
}
This hook is used like this:
const f = useLastVersion(() => { // do stuff and depends on props })
Basically, compared to const f = useCallBack(() => { // do stuff }, [dep1, dep2]) this avoids to declare the list of dependencies and f never changes, even if one of the dependency changes.
I don't know what to think about this code. I don't understand what are the disadvantages of using useLastVersion compared to useCallback.
That question is actually already more or less answered in the documentation: https://reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback
The interesting part is:
Also note that this pattern might cause problems in the concurrent mode. We plan to provide more ergonomic alternatives in the future, but the safest solution right now is to always invalidate the callback if some value it depends on changes.
Also interesting read: https://github.com/facebook/react/issues/14099 and https://github.com/reactjs/rfcs/issues/83
The current recommendation is to use a provider to avoid to pass callbacks in props if we're worried that could engender too many rerenders.
My point of view as stated in the comments, that this hook is redundant in terms of "how many renders you get", when there are too frequent dependencies changes (in useEffect/useCallback dep arrays), using a normal function is the best option (no overhead).
This hook hiding the render of the component using it, but the render comes from the useEffect in its parent.
If we summarize the render count we get:
Ref + useCallback (the hook): Render in Component (due to value) + Render in hook (useEffect), total of 2.
useCallback only: Render in Component (due to value) + render in Counter (change in function reference duo to value change), total of 2.
normal function: Render in Component + render in Counter : new function every render, total of 2.
But you get additional overhead for shallow comparison in useEffect or useCallback.
Practical example:
function App() {
const [value, setValue] = useState("");
return (
<div>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
type="text"
/>
<Component value={value} />
</div>
);
}
function useLastVersion(func) {
const ref = useRef();
useEffect(() => {
ref.current = func;
console.log("useEffect called in ref+callback");
}, [func]);
return useCallback((...args) => {
return ref.current(...args);
}, []);
}
function Component({ value }) {
const f1 = useLastVersion(() => {
alert(value.length);
});
const f2 = useCallback(() => {
alert(value.length);
}, [value]);
const f3 = () => {
alert(value.length);
};
return (
<div>
Ref and useCallback:{" "}
<MemoCounter callBack={f1} msg="ref and useCallback" />
Callback only: <MemoCounter callBack={f2} msg="callback only" />
Normal: <MemoCounter callBack={f3} msg="normal" />
</div>
);
}
function Counter({ callBack, msg }) {
console.log(msg);
return <button onClick={callBack}>Click Me</button>;
}
const MemoCounter = React.memo(Counter);
As a side note, if the purpose is only finding the length of input with minimum renders, reading inputRef.current.value would be the solution.
I'm attempting to build a form in React that has some auto-filling features. I'm trying to create the ability to 'lock' the value of LossOver50k to either 'Yes' or 'No' based on the ReplacementCost value to prevent input errors. The code I have right now is
import React, { useState } from "react";
import ReplacementCost from "./ReplacementCost";
import LossOver50K from "./LossOver50K";
const LossAmount = () => {
const [replacementCost, setReplacementCost] = useState("");
const [highValue, setHighValue] = useState("No");
const handleChange = (newReplacementCost) => {
setReplacementCost(newReplacementCost);
if (replacementCost >= 50000) {
setHighValue("Yes");
}
};
// const changeHighValue = (newHighValue) => {
// setHighValue(newHighValue);
// };
return (
<div>
<ReplacementCost value={replacementCost} onChange={handleChange} />
<LossOver50K value={highValue} />
</div>
);
};
export default LossAmount;
My commented piece was working as an onChange handler on the LossOver50k component but I'm trying to manipulate the value of LossOver50k if the value of Replacement Cost is equal to or greater than 50,000. I have each of these components successfully updating their state when I input the values directly by passing the props down to the child components. Any ideas?
TIA!
Your condition should be with the most updated value and not with a staled one (as setState is async):
const handleChange = (newReplacementCost) => {
setReplacementCost(newReplacementCost);
if (newReplacementCost >= 50000) {
setHighValue("Yes");
}
};
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
const InputComponent = ({ item, data }) => {
const [value, setValue] = useState('');
// Binding values for Edit based on unique Key
useEffect(() => {
if (data && data[item.field] && data[item.field] !== 'undefined') {
setValue(data[item.field]);
}
}, [data,item.field]);
//On change setting state
const setState = e => {
setValue(e.target.value);
};
return (
<div className='Input-Containter' data-test='InputBox-tbl-evt'>
<input
data-test='input-ele'
type='text'
value={value}
onChange={e => setState(e)}
/>
</div>
);
};
InputComponent.propTypes = {
item: PropTypes.object,
data: PropTypes.object
};
InputComponent.defaultProps = {
data: {
id: '1',
name: 'd'
},
item:{
field: 'id',
}
};
export default InputComponent;
Can someone help me How to test for setValue() in useEffect
-> Updated complete code for this Component
-> Component will take some data for binding values into input element & in the same
component we can edit values in it as-well.
First, let's take closer look onto useEffect section. What does it mean?
if any of prop is changed
and combination of new values are given some meaningful value(not undefined)
we initialize input's value based on those props even if we have to override custom value
How to test that? Changing prop(s) and validating input's value.
Based on that we may have up to 3(changed only first prop, only second or both) * 2 (if result is undefined or not) * 2 (if there has been already custom value provided and stored in useState or not) = 12. But I believe exhaustive testing is not a good way. So I'd put most checks in single test case:
it('re-init value for nested input after props changes', () => {
const wrapper = mount(<InputComponent />);
function getInput(wrapper) {
return wrapper.find("input").prop("value");
}
expect(getInput(wrapper).props("value")).toEqual("1"); // based on default props
getInput(wrapper).props().onChange({ target: {value: "initial"} }); // imitating manual change
expect(getInput(wrapper).props("value")).toEqual("initial");
wrapper.setProps({data: {a: "some-a", b: "undefined"} });
expect(getInput(wrapper).props("value")).toEqual("initial");
wrapper.setProps({ item: { field: "c" } }); // data[item.field] is undefined
expect(getInput(wrapper).props("value")).toEqual("initial");
wrapper.setProps({ item: {field: "b" } }); // data[item.field] is "undefined"
expect(getInput(wrapper).props("value")).toEqual("initial");
wrapper.setProps({ item: {field: "a" } }); // data[item.field] is meaningful
expect(getInput(wrapper).props("value")).toEqual("some-a");
});
As for getValue helper - it's needed cause we cannot just memoize input element itself like:
const wrapper = mount(...);
const input = wrapper.find("input");
...
expect(input.prop("value")).toEqual();
...
expect(input.prop("value")).toEqual();
Details can be found in Enzyme's docs. Or just know we need to re-run find after any update.
Also beware Enzyme's setProps does not replace current props but update them by merging(as well as React's setState does with state).
I created useBanner hooks
const useBanner = (array, yardage) => {
const [bannArr, setBannArr] = useState(array.slice(0, yardage));
const [bannListIndex, setBannIndex] = useState(1);
return {
....
};
};
Am I doing the right thing and the props throw in useState.
It’s permissible to use useBanner.
const Banner= ({
array,
yardage
}) => {
const { bannForth, bannBeck, bannArr } = useBanner(array, yardage);
return (
...
);
};
when props will change here.
Will change the state in useBanner.
or is it considered anti-patterns I have to write all this in useMemo
const useBanner = (array, yardage) => {
const [bannArr, setBannArr] = useState([]);
const [bannListIndex, setBannIndex] = useState(1);
useMemo(() => {
setBannArr(array.slice(0, yardage));
setBannIndex(1);
}, [array, yardage]);
return {
....
};
};
Yes, custom hooks are possible in React. Here is separate document discussing custom hooks.
But exactly you sample may require additional code depending on what is your final goal.
If you want initialize state only once, when component Banner is first created, you can just do as in your first sample
const Banner= ({
array,
yardage
}) => {
const { bannForth, bannBeck, bannArr } = useBanner(array, yardage);
return (
...
);
};
This will work perfectly. But if props array and yardage will change, this will not be reflected in component. So props will be used only once as initial values and then will not be used in useBanner even if changed (And it doesn't matter whether you'll use useBanner or useState directly). This answer highlight this.
If you want to update inital values on each props change, you can go with useEffect like below
const Banner= ({
array,
yardage
}) => {
const { bannForth, bannBeck, bannArr, setBannArr } = useBanner(array, yardage);
useEffect (() => {
// setBannArr should also be returned from useBanner. Or bannArr should be changed with any other suitable function returned from useBanner.
setBannArr(array.slice(0, yardage));
}, [array, yardage, setBannArr])
return (
...
);
};
In this case Banner component can control state itself and when parent component change props, state in Banner component will be reset to new props.
Here is small sample to showcase second option.