This is a follow-up to Refactoring class component to functional component with hooks, getting Uncaught TypeError: func.apply is not a function
I've declared a functional component Parameter that pulls in values from actions/reducers using the useSelector hook:
const Parameter = () => {
let viz = useSelector(state => state.fetchDashboard);
const parameterSelect = useSelector(state => state.fetchParameter)
const parameterCurrent = useSelector(state => state.currentParameter)
const dispatch = useDispatch();
const drawerOpen = useSelector(state => state.filterIconClick);
const handleParameterChange = (event, valKey, index, key) => {
parameterCurrent[key] = event.target.value;
return (
prevState => ({
...prevState,
parameterCurrent: parameterCurrent
}),
() => {
viz
.getWorkbook()
.changeParameterValueAsync(key, valKey)
.then(function () {
//some code describing an alert
});
})
.otherwise(function (err) {
alert(
//some code describing a different alert
);
});
}
);
};
const classes = useStyles();
return (
<div>
{drawerOpen ? (
Object.keys(parameterSelect).map((key, index) => {
return (
<div>
<FormControl component="fieldset">
<FormLabel className={classes.label} component="legend">
{key}
</FormLabel>
{parameterSelect[key].map((valKey, valIndex) => {
return (
<RadioGroup
aria-label="parameter"
name="parameter"
value={parameterCurrent[key]}//This is where the change should be reflected in the radio button
onChange={(e) => dispatch(
handleParameterChange(e, valKey, index, key)
)}
>
<FormControlLabel
className={classes.formControlparams}
value={valKey}
control={
<Radio
icon={
<RadioButtonUncheckedIcon fontSize="small" />
}
className={clsx(
classes.icon,
classes.checkedIcon
)}
/>
}
label={valKey}
/>
</RadioGroup>
);
})}
</FormControl>
<Divider className={classes.divider} />
</div>
);
})
) : (
<div />
)
}
</div >
)
};
export default Parameter;
What I need to have happen is for value={parameterCurrent[key]} to rerender on handleParameterChange (the handleChange does update the underlying dashboard data, but the radio button doesn't show as being selected until I close the main component and reopen it). I thought I had a solution where I forced a rerender, but because this is a smaller component that is part of a larger one, it was breaking the other parts of the component (i.e. it was re-rendering and preventing the other component from getting state/props from it's reducers). I've been on the internet searching for solutions for 2 days and haven't found anything that works yet. Any help is really apprecaited! TIA!
useSelector() uses strict === reference equality checks by default, not shallow equality.
To use shallow equal check, use this
import { shallowEqual, useSelector } from 'react-redux'
const selectedData = useSelector(selectorReturningObject, shallowEqual)
Read more
Ok, after a lot of iteration, I found a way to make it work (I'm sure this isn't the prettiest or most efficient, but it works, so I'm going with it). I've posted the code with changes below.
I added the updateState and forceUpdate lines when declaring the overall Parameter function:
const Parameter = () => {
let viz = useSelector(state => state.fetchDashboard);
const parameterSelect = useSelector(state => state.fetchParameter)
const parameterCurrent = useSelector(state => state.currentParameter);
const [, updateState] = useState();
const forceUpdate = useCallback(() => updateState({}), []);
const dispatch = useDispatch();
const drawerOpen = useSelector(state => state.filterIconClick);
Then added the forceUpdate() line here:
const handleParameterChange = (event, valKey, index, key) => {
parameterCurrent[key] = event.target.value;
return (
prevState => ({
...prevState,
parameterCurrent: parameterCurrent
}),
() => {
viz
.getWorkbook()
.changeParameterValueAsync(key, valKey)
.then(function () {
//some code describing an alert
});
})
.otherwise(function (err) {
alert(
//some code describing a different alert
);
});
forceUpdate() //added here
}
);
};
Then called forceUpdate in the return statement on the item I wanted to re-render:
<RadioGroup
aria-label="parameter"
name="parameter"
value={forceUpdate, parameterCurrent[key]}//added forceUpdate here
onChange={(e) => dispatch(
handleParameterChange(e, valKey, index, key)
)}
>
I've tested this, and it doesn't break any of the other code. Thanks!
Related
I have React component where I can dynamically add new text inputs. So, I need to push the values from the inputs to array.
Can anyone help me how to do this?
Here is my code:
function FormPage({ setData }) {
const [item, setItem] = useState([]);
const [counter, setCounter] = useState(0);
const handleCounter = () => {
setCounter(counter + 1);
};
const addItem = (setItem) => setItem((ing) => [...ing, newItem]);
return (
{Array.from(Array(counter)).map((c, index) =>
<TextField
key={index}
label="Item"
onChange={() => setItem(i=> [...i, (this.value)])}
/>
)}
<Button onClick={handleCounter}>Add one more item</Button>
)
}
Here is example in sandbox:
https://codesandbox.io/s/solitary-sound-t2cfy?file=/src/App.js
Firstly, you are using two-way data binding with your TextField component, so you also need to pass a value prop.
Secondly, to get the current value of TextField, we don't use this.value. Rather, the callback to onChange takes an argument of type Event and you can access the current value as follows
<TextField
...
onChange={(e) => {
const value = e.target.value;
// Do something with value
}}
/>
You cannot return multiple children from a component without wrapping them by single component. You are simply returning multiple TextField components at the same level, which is also causing an error. Try wrapping them in React.Fragment as follows
...
return (
<React.Fragment>
{/* Here you can return multiple sibling components*/}
</React.Fragment>
);
You are mapping the TextField components using counter which is equal to the length of item array. In handleCounter, we'll add a placeholder string to accomodate the new TextField value.
...
const handleCounter = () => {
setCounter(prev => prev+1); // Increment the counter
setItem(prev => [...prev, ""]); // Add a new value placeholder for the newly added TextField
}
return (
<React.Fragment>
{ /* Render only when the value of counter and length of item array are the same */
counter === item.length && (Array.from(Array(counter).keys()).map((idx) => (
<TextField
key={idx}
value={item[idx]}
label="Item"
onChange={(e) => {
const val = e.target.value;
setItem(prev => {
const nprev = [...prev]
nprev[idx] = val;
return nprev;
})
}}
/>
)))}
<br />
<Button onClick={handleCounter}>Add one more item</Button>
</React.Fragment>
);
Here is the sandbox link
Try this:
import "./styles.css";
import React, { useState } from "react";
export default function App() {
// Changes made here
const [item, setItem] = useState({});
const [counter, setCounter] = useState(0);
console.log("item 1:", item[0], "item 2:", item[1],item);
const handleCounter = () => {
setCounter(counter + 1);
};
const addItem = (newItem) => setItem((ing) => [...ing, newItem]);
return (
<>
{Array.from(Array(counter)).map((c, index) => (
<input
type="text"
key={index}
//Changes made here
value={item[index]}
label="Item"
// Changes made here
onChange={(event) => setItem({...item, [index]:event.target.value })}
/>
))}
<button onClick={handleCounter}>Add one more item</button>
</>
);
}
Instead of using an array to store the input values I recommend using an object as it's more straight-forward.
If you wanted to use an array you can replace the onChange event with the following:
onChange={(event) => {
const clonedArray = item.slice()
clonedArray[index] = event.target.value
setItem(clonedArray)
}}
It's slightly more convoluted and probably slightly less optimal, hence why I recommend using an object.
If you want to loop through the object later you can just use Object.entries() like so:
[...Object.entries(item)].map(([key, value]) => {console.log(key, value)})
Here's the documentation for Object.entries().
codeSolution: https://codesandbox.io/s/snowy-cache-dlnku?file=/src/App.js
import "./styles.css";
import React, { useState } from "react";
export default function App() {
const [item, setItem] = useState(["a", "b"]);
const handleCounter = () => {
console.log(item, "item");
setItem([...item, ""]);
};
const setInput = (index) => (evt) => {
item.splice(index, 1, evt.target.value);
setItem([...item]);
};
return (
<>
{item.map((c, index) => {
return (
<input
type="text"
key={index}
label="Item"
value={c}
onChange={setInput(index)}
/>
);
})}
<button onClick={handleCounter}>Add one more item</button>
</>
);
}
I have solved for you . check if this works for you , if any issues tell me
I wrote a program that takes and displays contacts from an array, and we have an input for searching between contacts, which we type and display the result.
I used if in the search function to check if the searchKeyword changes, remember to do the filter else, it did not change, return contacts and no filter is done
I want to do this control with useEffect and I commented on the part I wrote with useEffect. Please help me to reach the solution of using useEffect. Thank you.
In fact, I want to use useEffect instead of if
I put my code in the link below
https://codesandbox.io/s/simple-child-parent-comp-forked-4qf39?file=/src/App.js:905-913
Issue
In the useEffect hook in your sandbox you aren't actually updating any state.
useEffect(()=>{
const handleFilterContact = () => {
return contacts.filter((contact) =>
contact.fullName.toLowerCase().includes(searchKeyword.toLowerCase())
);
};
return () => contacts;
},[searchKeyword]);
You are returning a value from the useEffect hook which is interpreted by React to be a hook cleanup function.
See Cleaning up an effect
Solution
Add state to MainContent to hold filtered contacts array. Pass the filtered state to the Contact component. You can use the same handleFilterContact function to compute the filtered state.
const MainContent = ({ contacts }) => {
const [searchKeyword, setSearchKeyword] = useState("");
const [filtered, setFiltered] = useState(contacts.slice());
const setValueSearch = (e) => setSearchKeyword(e.target.value);
useEffect(() => {
const handleFilterContact = () => {
if (searchKeyword.length >= 1) {
return contacts.filter((contact) =>
contact.fullName.toLowerCase().includes(searchKeyword.toLowerCase())
);
} else {
return contacts;
}
};
setFiltered(handleFilterContact());
}, [contacts, searchKeyword]);
return (
<div>
<input
placeholder="Enter a keyword to search"
onChange={setValueSearch}
/>
<Contact contacts={contacts} filter={filtered} />
</div>
);
};
Suggestion
I would recommend against storing a filtered contacts array in state since it is easily derived from the passed contacts prop and the local searchKeyword state. You can filter inline.
const MainContent = ({ contacts }) => {
const [searchKeyword, setSearchKeyword] = useState("");
const setValueSearch = (e) => setSearchKeyword(e.target.value);
const filterContact = (contact) => {
if (searchKeyword.length >= 1) {
return contact.fullName
.toLowerCase()
.includes(searchKeyword.toLowerCase());
}
return true;
};
return (
<div>
<input
placeholder="Enter a keyword to search"
onChange={setValueSearch}
/>
<Contact contacts={contacts.filter(filterContact)} />
</div>
);
};
I am experimenting with ReactJS custom Hooks, but I don't understand what's happening in the example below!
I expect to see on the screen: 'Label: <followed by one of the selected option ("Bananas" or "Apples" or "Oranges")>', but it is 'Label: ' so the optionis undefined!
Could someone explain me what's happening under the hood, why I cannot see the expected output for the option ?
const useFruit = () => {
const [option, setOption] = useState<string>();
const [options] = useState(["Bananas", "Apples", "Oranges"]);
return {
option,
setOption,
options,
};
};
const FruitDropdown = () => {
const { options, setOption } = useFruit();
return (
<select
placeholder="Select option"
onChange={(e) => {
setOption(e.target.value);
}}
>
{options.map((option) => (
<option value={option}>{option}</option>
))}
</select>
);
};
const FruitLabel = () => {
const { option } = useFruit();
return (
<label>Label: {option}</label>
);
};
export default function play() {
return (
<>
<FruitDropdown />
<FruitLabel />
</>
);
}
Just because they are using the same custom hook, they are not automatically sharing state. Every time you run useFruits you will create a new isolated state which is only accessable in that instance how the hook. And whenever a the state is created it defaults to undefined.
What you need in order to solve your problem is to wrap your components inside a context and place the state inside the context. Something like this:
const FruitContext = createContext()
const FruitProvider = ({ children }) => {
const [option, setOption] = useState<string>();
const [options] = useState(["Bananas", "Apples", "Oranges"]);
return (
<FruitContext.Provider value={{ option, setOption, options }}>{children}</FruitContext.Provider>
)
}
export const useFruits = () => useContext(FruitContext)
Dont forget to wrap your components:
<FruitProvider>
<FruitDropdown />
<FruitLabel />
</FruitProvider>
Child component
export const FlightRange = (props) => {
const [value, setValue] = useState(props.value);
return (
<>
<input
type='range'
min={1000}
max={50000}
step="500"
value={value}
onChange={(e) => {
setValue(e.target.value);
props.handleSliderChange(value);
}}
/>
<span>{value}</span>
</>
);
};
parent component
useEffect(() => {
const result = axios.get('http://localhost:8000/')
.then((res) => res.json())
.then((data) => {
const flightData = data.filter((value) => {
return (
valuesplit(' ')[1] < priceSlider
);
});
})
}, [priceSlider]);
return(
<Child value={priceSlider} handleSliderChange={(value)=> setPriceSlider(value)} />
)
useEffect does not get called when the slider is changed the first time. It gets called a second time with the stale (previous value) value.
What am I missing?
in onChange you need to call like this
onChange={(e) => {
setValue(e.target.value);
props.handleSliderChange(e.target.value);
}}
since value is not updated instantly when you call setValue(e.target.value); , value will have previous value that you are passing in props.handleSliderChang(value)
to know how setState works see this answer
The issue is on the onClick callback of FlightRange input, see comments on code below
onChange = {(e) => {
setValue(e.target.value); // this is async
// therefore, the value you are passing here is not the same as e.target.value but simply the value before setting the state
props.handleSliderChange(value);
}}
So to fix this just refactor props.handleSliderChange argument to e.target.value
onChange = {(e) => {
setValue(e.target.value);
props.handleSliderChange(e.target.value);
}}
It because the child is having it's own life cycle since you are using useState in child. so whatever props you pass to your child, the child's state won't affected.
plus you are passing incorrect value in onChange
Solution: just use the props value directly on child (do not store in state):
export const FlightRange = (props) => {
const { value, handleSliderChange } = props;
return (
<>
<input
type='range'
min={1000}
max={50000}
step="500"
value={value}
onChange={(e) => {
handleSliderChange(e.target.value);
}}
/>
<span>{value}</span>
</>
);
};
I have a set of buttons in a child component where when clicked set a corresponding state value true or false. I have a useEffect hook in this child component also with dependencies on all these state values so if a button is clicked, this hook then calls setFilter which is passed down as a prop from the parent...
const Filter = ({ setFilter }) => {
const [cycling, setCycling] = useState(true);
const [diy, setDiy] = useState(true);
useEffect(() => {
setFilter({
cycling: cycling,
diy: diy
});
}, [cycling, diy]);
return (
<Fragment>
<Row>
<Col>
<Button block onClick={() => setCycling(!cycling)}>cycling</Button>
</Col>
<Col>
<Button block onClick={() => setdIY(!DIY)}>DIY</Button>
</Col>
</Row>
</Fragment>
);
};
In the parent component I display a list of items. I have two effects in the parent, one which does an initial load of items and then one which fires whenever the filter is changed. I have removed most of the code for brevity but I think the ussue I am having boils down to the fact that on render of my ItemDashboard the filter is being called twice. How can I stop this happening or is there another way I should be looking at this.
const ItemDashboard = () => {
const [filter, setFilter] = useState(null);
useEffect(() => {
console.log('on mount');
}, []);
useEffect(() => {
console.log('filter');
}, [filter]);
return (
<Container>..
<Filter setFilter={setFilter} />
</Container>
);
}
I'm guessing, you're looking for the way to lift state up to common parent.
In order to do that, you may bind event handlers of child components (passed as props) to desired callbacks within their common parent.
The following live-demo demonstrates the concept:
const { render } = ReactDOM,
{ useState } = React
const hobbies = ['cycling', 'DIY', 'hiking']
const ChildList = ({list}) => (
<ul>
{list.map((li,key) => <li {...{key}}>{li}</li>)}
</ul>
)
const ChildFilter = ({onFilter, visibleLabels}) => (
<div>
{
hobbies.map((hobby,key) => (
<label {...{key}}>{hobby}
<input
type="checkbox"
value={hobby}
checked={visibleLabels.includes(hobby)}
onChange={({target:{value,checked}}) => onFilter(value, checked)}
/>
</label>))
}
</div>
)
const Parent = () => {
const [visibleHobbies, setVisibleHobbies] = useState(hobbies),
onChangeVisibility = (hobby,visible) => {
!visible ?
setVisibleHobbies(visibleHobbies.filter(h => h != hobby)) :
setVisibleHobbies([...visibleHobbies, hobby])
}
return (
<div>
<ChildList list={visibleHobbies} />
<ChildFilter onFilter={onChangeVisibility} visibleLabels={visibleHobbies} />
</div>
)
}
render (
<Parent />,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><div id="root"></div>
Yes, you can, useEffect in child component which depends on the state is also how you typically implement a component which is controlled & uncontrolled:
const NOOP = () => {};
// Filter
const Child = ({ onChange = NOOP }) => {
const [counter, setCounter] = useState(0);
useEffect(() => {
onChange(counter);
}, [counter, onChange]);
const onClick = () => setCounter(c => c + 1);
return (
<div>
<div>{counter}</div>
<button onClick={onClick}>Increase</button>
</div>
);
};
// ItemDashboard
const Parent = () => {
const [value, setState] = useState(null);
useEffect(() => {
console.log(value);
}, [value]);
return <Child onChange={setState} />;
};