Stop checkbox values being reset after page load - javascript

Hello I am working on a pop up window where the user can filter a table of data.
The filter is selected using checkboxes.
My issue:
On page load there is a useEffect that changes every checkbox to false. This is based on the props coming in from the API.
I'd like on page load (and when the filter opens) that the checkbox state is stored based on what the user has selected previously in their session
code:
Filter component*
[...]
import FilterSection from "../FilterSection";
const Filter = ({
open,
handleClose,
setFilterOptions,
[..]
roomNumbers,
}) => {
const [roomValue, setRoomValue] = React.useState();
const [roomListProp, setRoomListProp] = React.useState(); // e.g. [["roomone", false], ["roomtwo", true]];
const sendRoomFilterData = (checkedRoomsFilterData) => {
setRoomValue(checkedRoomsFilterData);
};
const setCheckboxListPropRoom = (data) => {
setRoomListProp(data);
};
// extract, convert to an object and pass back down? or set local storage and get
// local storage and pass back down so that we can get it later?
const convertToLocalStorageFilterObject = (roomData) => { // []
if (roomData !== undefined) {
const checkedRooms = roomData.reduce((a, curval) => ({ ...a, [curval[0]]: curval[1] }), {});
localStorage.setItem("preserved", JSON.stringify(checkedRooms)); // sets in local storage but values get wiped on page load.
}
};
React.useEffect(() => {
const preservedFilterState = convertToLocalStorageFilterObject(roomListProp);
}, [roomListProp]);
const applyFilters = () => {
setFilterOptions([roomValue]);
handleClose();
};
const classes = CurrentBookingStyle();
return (
<Dialog
fullWidth
maxWidth="sm"
open={open}
onClose={() => handleClose(false)}
>
<DialogTitle>Filter By:</DialogTitle>
<DialogContent className={classes.margin}>
<FilterSection
filterName="Room number:"
filterData={roomNumbers}
setFilterOptions={sendRoomFilterData}
setCheckboxListProp={setCheckboxListPropRoom}
/>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={applyFilters}>
Apply Filters
</Button>
</DialogActions>
</Dialog>
);
};
Filter Section used in Filter
import {
TableCell,
Typography,
FormControlLabel,
Checkbox,
FormGroup,
} from "#material-ui/core";
const FilterSection = ({
filterData, filterName, setFilterOptions, setCheckboxListProp
}) => {
const [checkboxValue, setCheckboxValue] = React.useState({});
const [checkboxFilterList, setCheckboxFilterList] = React.useState([]);
const handleCheckboxChange = (event) => {
setCheckboxValue({
...checkboxValue,
[event.target.name]: event.target.checked, // room1: true
});
};
const = () => filterData // ["room1" "room2"]; comes from API
.filter((val) => !Object.keys(checkboxValue).includes(val))
.reduce((acc, currval) => ({
...acc, [currval]: false, // converts array to object and sets values to false
}), checkboxValue);
React.useEffect(() => {
const transformedCheckboxListItems = Object.entries(convertToObject());
setCheckboxFilterList(transformedCheckboxListItems);
setFilterOptions(transformedCheckboxListItems.filter(([, val]) => val).map(([key]) => key));
setCheckboxListProp(transformedCheckboxListItems);
}, [checkboxValue]);
return (
<>
<Typography style={{ fontWeight: "bold" }}>{filterName}</Typography>
<FormGroup row>
{checkboxFilterList.map(([key, val]) => (
<TableCell style={{ border: 0 }}>
<FormControlLabel
control={(
<Checkbox
checked={val}
onChange={handleCheckboxChange}
name={key}
color="primary"
/>
)}
label={key}
/>
</TableCell>
))}
</FormGroup>
</>
);
};
What i have tried:
I have created a reusable component called "FilterSection" which takes takes data from the API "filterData" and transforms it from an array to an object to set the initial state for the filter checkboxes.
On page load of the filter I would like the checkboxes to be true or false depending on what the user has selected, however this does not work as the convertToObject function in my FilterSection component converts everything to false again on page load. I want to be able to change this but not sure how? - with a conditional?
I have tried to do this by sending up the state for the selected checkboxes to the Filter component then setting the local storage, then the next step would be to get the local storage data and somehow use this to set the state before / after page load. Unsure how to go about this.
Thanks in advance

I am not sure if I understand it correctly, but let me have a go:
I have no idea what convertToObject does, but I assume it extracts the saved filters from localStorage and ... updates the filter value that has just been changed?
Each time the FilterSection renders for the first time, checkboxValue state is being initialised and an useEffect runs setCheckboxListProp, which clears the options, right?
If this is your problem, try running setCheckboxListProp directly in the handleCheckboxChange callback rather than in an useEffect. This will ensure it runs ONLY after the value is changed by manual action and not when the checkboxValue state is initialised.

I solved my problem by moving this line:
const [checkboxValue, setCheckboxValue] = React.useState({});
outside of the component it was in because every time the component re-rendered it ran the function (convertToObject() which reset each checkbox to false
by moving the state for the checkboxes up three layers to the parent component, the state never got refreshed when the or component pop up closed. Now the checkbox data persists which is the result I wanted.
:D

Related

How do I set a state to a value in localStorage?

There is a button that toggles dark and light mode, and the state of what mode the page is on is saved in localStorage. However, I cannot change the initial value of the state (dark) and I don't know why. This is done in a useEffect function but no matter what the value of dark is, it is always set to its initial value of false.
How do I set the value of the localStorage to the dark state?
function Mode() {
const [dark, setDark] = useState(false);
// localStorage.removeItem("dark");
const onClick = () => {
if (dark) {
setDark(false);
document.querySelector("body").classList.remove("dark");
} else {
setDark(true);
document.querySelector("body").classList.add("dark");
}
localStorage.setItem("dark", dark);
};
const localDark = JSON.parse(localStorage.getItem("dark"));
useEffect(() => {
if (localDark !== null) {
setDark(!JSON.parse(localStorage.getItem("dark"))); // this is what does not change the value of dark
onClick();
}
}, []);
return (
<div onClick={onClick} className="mode">
{dark ? <Light /> : <Dark />}
</div>
);
}
Directly use the value from localStorage in useState as the default. useEffect is unnecessary here.
const [dark, setDark] = useState(JSON.parse(localStorage.getItem("dark")));
document.body.classList.toggle('dark', dark);
The click event handler should set the localStorage dark value to the logical complement of the current value.
const onClick = () => {
localStorage.setItem("dark", !dark);
setDark(!dark);
};
Use a function to initialize the state from local storage. Update the storage and the body's class on init, and when dark state changes:
const getLocalDark = () => !!JSON.parse(localStorage.getItem("dark"));
function Mode() {
const [dark, setDark] = useState(getLocalDark);
const onClick = () => {
setDark(d => !d);
};
useEffect(() => {
const classList = document.querySelector("body").classList;
if (dark) classList.add("dark");
else classList.remove("dark");
localStorage.setItem("dark", dark);
}, [dark]);
return (
<div onClick={onClick} className="mode">
{dark ? <Light /> : <Dark />}
</div>
);
}
Perhaps you'd be interested in a useLocalStorage hook. Here's how that can be implemented:
export const useLocalStorage = (key, initialState) => {
const [value, setValue] = useState(() => {
// Initialize with the value in localStorage if it
// already exists there.
const data = localStorage.getItem(key);
// Otherwise, initialize using the provided initial state.
return data ? JSON.parse(data) : initialState;
});
// Each time "value" is changed using "setValue", update the
// value in localStorage to reflect these changes.
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [value]);
return [value, setValue];
};
This hook syncs the value seen in localStorage with the value stored in memory under the value variable.
The usage looks like this (almost identical to regular useState):
export const Counter = () => {
const [count, setCount] = useLocalStorage('count', 0);
return (
<div>
<p>{count}</p>
<button
onClick={() => {
setCount((prev) => prev + 1);
}}>
Increase count
</button>
</div>
);
};
However, the main caveat of this hook is that it's only really meant to be used in one component. That means, if you change the value from light to dark in one component using this hook, any other components using it won't be updated. So instead, you should look into using a similar implementation of what is demonstrated above, but using the React Context API. That way, you'll ensure your in-memory values are in sync with those stored in localStorage. Theming is one of the main uses of the Context API.
Good luck! :)

Updating material ui select options from input

I'm new to Reactjs, here i have material ui select element, as you can see i have default values for select element, and also by clicking 'ADD USER' button and submitting, i can add new values to select element, and from select element i can also delete options, my question here is how can i edit specific option from select element, i have added EditUser component for that when option is clicked, but dont know how to update it, any advice ?
my code:
https://codesandbox.io/s/material-ui-multiple-select-with-select-all-option-forked-ysglz8?file=/src/AddUser.js
App.js:
import React, { useState } from "react";
import Checkbox from "#material-ui/core/Checkbox";
import InputLabel from "#material-ui/core/InputLabel";
import ListItemIcon from "#material-ui/core/ListItemIcon";
import ListItemText from "#material-ui/core/ListItemText";
import MenuItem from "#material-ui/core/MenuItem";
import FormControl from "#material-ui/core/FormControl";
import Select from "#material-ui/core/Select";
import DeleteIcon from "#material-ui/icons/Delete";
import CreateIcon from "#material-ui/icons/Create";
import { MenuProps, useStyles } from "./utils";
import AddUser from "./AddUser";
import {
Button,
List,
ListItem,
Dialog,
DialogTitle,
DialogContent
} from "#material-ui/core";
import EditUser from "./EditUser";
function App() {
const rawOptions = [
"Oliver Hansen",
"Van Henry",
"April Tucker",
"Ralph Hubbard",
"Omar Alexander",
"Carlos Abbott",
"Miriam Wagner",
"Bradley Wilkerson",
"Virginia Andrews",
"Kelly Snyder"
];
const classes = useStyles();
const [selected, setSelected] = useState([]);
const [options, setOptions] = useState(rawOptions);
const [openAddModal, setOpenAddModal] = useState(false);
const [openUpdateModal, setOpenUpdateModal] = useState(false);
const handleChange = (event) => {
console.log("vals", event.target);
const value = event.target.value;
setSelected(value);
console.log("values", selected);
};
function addUser(newArray) {
setOptions(newArray);
}
const openAddUser = () => {
setOpenAddModal(true);
};
const openUpdateUser = (e) => {
e.stopPropagation();
setOpenUpdateModal(true);
};
const closeAddModal = () => {
setOpenAddModal(false);
};
const closeUpdateModal = () => {
setOpenUpdateModal(false);
};
const updateUser = (updateUser) => {
setOptions(updateUser);
};
return (
<FormControl className={classes.formControl}>
<div>
<InputLabel id="mutiple-select-label">Multiple Select</InputLabel>
<Select
labelId="mutiple-select-label"
multiple
variant="outlined"
value={selected || []}
onChange={handleChange}
renderValue={(selected) => selected}
MenuProps={MenuProps}
>
{options.map((option, index) => (
<MenuItem key={option.id} value={option}>
<ListItemIcon>
<Checkbox checked={selected?.includes(option)} />
</ListItemIcon>
<ListItemText primary={option.title}>{option}</ListItemText>
<DeleteIcon
onClick={(e) => {
e.stopPropagation();
setOptions(options.filter((o) => o !== option));
console.log("run");
}}
/>
<ListItemIcon>
<CreateIcon onClick={openUpdateUser} />
</ListItemIcon>
</MenuItem>
))}
</Select>
<Button onClick={openAddUser} style={{ backgroundColor: "#287B7A" }}>
Add User
</Button>
</div>
<p>{selected}</p>
<AddUser
openAddModal={openAddModal}
handleClose={closeAddModal}
array={options}
addUser={addUser}
/>
<EditUser
openUpdateModal={openUpdateModal}
handleClose={closeUpdateModal}
array={options}
updateUser={updateUser}
/>
</FormControl>
);
}
export default App;
Currently your users haven't any ids or something that they can be identified with.
Try to make users array of objects like this
const rawOptions = [
{
id: 0,
name: "Oliver Hansen"
},
];
Make your inputs and etc. accept array of objects.
After this in your Edit component, you should pass there selected user object and set default state for your input value (so your can really edit it and not input a new value)
const [value, setValue] = useState(props.user.name);
And in your someFunction, that acts like handleSubmit function, pass your user object, or user id and new value. It will look like
const someFunction = (event) => {
event.preventDefault();
if (value) {
props.hanldeSubmit(props.user.id, value)
props.handleClose() // Might be better to put it into your handleSubmit in parent component
}
};
And finally in your App.js, create handleSubmit function that accepts user id and value, and modify your state in it. Find user by ID and put a new value.
Do not forget to pass this function into your EditUser component.
Hope that helped you!
UPDATE
Ooookay, so, you might also want to start with less hard examples, but lets stick to what we have. I'll note here some problems that I found, and explain how to make it work.
First of all, always name functions and variables correctly, you should understand what function or variable do only by its name (ideally), I understand that this is just an example code and etc, but this makes this point only more important, because when you learn something new, its good not to make it harder for yourself.
Second thing, just for some case, I don't know if this mistake or not, so I mention this:
<CreateIcon
onClick={() =>
openUpdateUser({ id: option.id, name: option.name })
}
/>
Here you pass object, and in openUpdateUser you accept e (event) as a first parameter. Just for you to know, you will get event in your anonymous function and it wouldn't be passed further in openUpdateUser, to pass it, you should write it like this:
<CreateIcon
onClick={(e) =>
openUpdateUser(e, { id: option.id, name: option.name })
}
/>
Okay, let's get back to business.
The first real problem here: You have your options in one state, and selected options in other, so when you add some user, you will see users from selected. What problem does it cause? When you will try to update user in your options, it might be updated, but you wouldn't see any changes in selected, because it two different states.
We will solve it by making one source of information. Now we will store in selected not users, but users ids and in render we will get users from our options by ids.
// before
<p>{selected}</p>
// after
{selected.map(selectedUserId => (
<div>{options.filter(option => option.id === selectedUserId)[0].name}</div>
))}
Now, any changes to options will affect your selected users. Also, update your code to add\remove ids and not user objects.
Let's go further, now you have your selected user and method for updating in edit component, let's go edit:
// EditUser.js
const [value, setValue] = useState(props.edit.name); // set user name as default value to edit it
function changeValue(e) {
setValue(e.target.value);
}
const someFunction = (event) => {
event.preventDefault();
if (value) {
props.updateUser({id: props.edit.id, name: value}) // Pass user id and new value to our update function
props.handleClose();
}
};
So, now we have our new value in update function, the only one thing left is to save those updates. We'll do it in easy way:
// Normally here would be some api call for user update
const updateUser = (updateUser) => {
const temp = [...options] // Not deep copy of our options
temp[temp.findIndex(user => user.id === updateUser.id)].name = updateUser.name;
setOptions(temp)
};
And thats all, now it should work as expected.
Also as improve, you can restructure your options array of objects to make it easier to modify data.
(yep, I know that it was my suggestion, but anyway :) )
Currently it looks like this:
const rawOptions = [
{
id: 0,
name: "Oliver Hansen"
},
];
We can make it object of objects, where key will be id of user:
const rawOptions = {
0: {
id: 0,
name: "Oliver Hansen"
},
};
//Now to get user you can just do
options[userId]
// To get users array
Object.values(options)
// To modify user
const updateUser = (updateUser) => {
setOptions({...options}, [updateUser.id]: updateUser)
};
Just like previously, I wouldn't make those changes into codesandbox, the best way to learn programming is to write some code by yourself :)
If you will find any other issues or questions, feel free to ask, hope it helps :)

how to fix form submission with useEffect hook (as is: need to click submit twice)

App takes user options and creates an array objects randomly, and based on user options. (it's a gamer tag generator, writing to learn react.js). As is, App is a functional component and I use useState to store array of objects (gamertags) and the current selected options.
I use formik for my simple form. It takes two clicks to get a new item with updated options. I know why, options in state of App doesn't not update until it rerenders as the function for form submission is async. Therefore, all of my options are updated, after the first click, and are correct with the second because they were updated with the rerendering and after I needed them.
I know the solution is to use a useEffect hook, but despite reading over other posts and tuts, I don't understand how to apply it. It's my first instance of needing that hook and I'm still learning.
I wrote a simplified App to isolate the problem as much as possible and debug. https://codesandbox.io/s/morning-waterfall-impg3?file=/src/App.js
export default function App() {
const [itemInventory, setItemInventory] = useState([
{ options: "apples", timeStamp: 123412 },
{ options: "oranges", timeStamp: 123413 }
]);
const [options, setOptions] = useState("apples");
const addItem = (item) => {
setItemInventory([item, ...itemInventory]);
};
const createItem = () => {
return { options: options, timeStamp: Date.now() };
};
class DisplayItem extends React.Component {
render() { // redacted for brevity}
const onFormUpdate = (values) => {
const newOption = values.options;
setOptions(newOption);
addItem(createItem());
};
const UserForm = (props) => {
return (
<div>
<Formik
initialValues={{
options: props.options
}}
onSubmit={async (values) => {
await new Promise((r) => setTimeout(r, 500));
console.log(values);
props.onUpdate(values);
}}
>
{({ values }) => (
<Form> //redacted for brevity
</Form>
)}
</Formik>
</div>
);
};
return (
<div className="App">
<div className="App-left">
<UserForm options={options} onUpdate={onFormUpdate} />
</div>
<div className="App-right">
{itemInventory.map((item) => (
<DisplayItem item={item} key={item.timeStamp} />
))}
</div>
</div>
);
}
This is probably a "layup" for you all, can you help me dunk this one? Thx!
Solved problem by implementing the useEffect hook.
Solution: The functions that create and add an item to the list, addItem(createItem()), become the first argument for the useEffect hook. The second argument is the option stored in state, [options]. The callback for the form, onFormUpdate only updates the option in state and no longer tries to alter state, i.e. create and add an item to the list. The useEffect 'triggers' the creation and addition of a new item, this time based on the updated option because the updated option is the second argument of the hook.
Relevant new code:
useEffect( () => {
addItem(createItem());
}, [options]);
const onFormUpdate = (values) => {
const newOption = values.options;
setOptions(newOption);
//addItem used to be here
};

React filter by category feature

Hello I am building photo gallery where I would like to add feature that user will be able filter by Category. I tried some solutions but there are two bugs that I am not able to fix. First is that if I go to the GalleryPage (using Swtich) it does NOT render dynamically added buttons from FilterButton component. I have to click one more time on the link and then it DOES render the buttons. I dont know why it does not work on the first render.
Other issue is that I am able to filter by category but it causes the infinite loop in the useEffect and I dont know how to fix it.
I have got GalleryPage component where I am getting data from API and parsing the data for using later in other components. Here it seems that is all working fine.
const GalleryPage = () => {
const url = 'someurl';
const [data, setData] = useState([]);
const [categoryList, setCategoryList] = useState([]);
const [category, setCategory] = useState('All');
useEffect(() => {
const fetchData = async () => {
const result = await axios(url,);
setData(result.data)
result.data.forEach(item => {
imageUrl.push(item.image)
if (categoryList.indexOf(item.group) === -1) {
categoryList.push(item.group)
}
})
}
fetchData();
}, [])
return (
<FilterButton setCategory={setCategory} categoryList={categoryList}/>
<Gallery data={data} category={category}/>
)
}
If I go to the GalleryPage the h3 and 'All' button is rendered. But I have to click on the link one more time to render the buttons inside the map function:
const FilterButton = ({setCategory, categoryList}) => {
return(
<h3>Gallery</h3>
<button onClick={()=> setCategory('All')}>All</button>
{categoryList.map(item => (
<button key={item} onClick={()=> setCategory(item)}>{item}</button>
))}
)
};
export default FilterButton;
And here I am not able to fix the infinite loop:
const Gallery = ({data, category}) => {
const [photos, setPhotos] = useState([]);
useEffect(()=>{
let temp = []
if (category === 'All'){
setPhotos(data)
}else{
data.map(item => {
temp.push(item)
})
}
setPhotos(temp)
})
return(
photos.map((item =>
<img key={item.id} src={item.image}/>
))
)
};
export default Gallery;
If I add empty array to the useEffect it does not work at all. Also I am using styled components and framer motion but it should not have affect on this I hope.
First, I see that you're never setting your state for categoryList.
After modifying categoryList, you should call setCategoryList() with the new category list. This way, the state variable will be 'remembered' when the component is re-rendered.
You can read about the useState hook here.
Additionally, for the useEffect hook, the 'empty array' you pass in at the end is actually an array of variables to 'watch' for changes. If you pass an empty array, the useEffect will only run once, at the first page load. However, you can pass in something like [category] so that the useEffect is only called when the category variable is modified, which I persume is what you want to do.

React child component not updating when props change

I'm trying to build out a local search in React consisting of a parent component with a search input, and a child component containing a list of search results. I've created a React state object that contains the search query and a list of search results. When the input field is changed, the search is ran to generate a new result set, and both properties (query and results) are updated. The search input is updating as expected, but the child component doesn't re-render despite an update to its prop. I've removed some of the code for brevity, but if you need more information please let me know.
export const Search = () => {
let [searchState, setSearchState] = React.useState({});
let handleChange = (event) => {
searchState['results'] = searchProducts(event.target.value);
searchState['query'] = event.target.value;
setSearchState(searchState);
};
return (
<div>
<FormControl>
<Input
value={searchState.query}
onChange={handleChange}>
Search...
</Input>
</FormControl>
<SearchResults results={searchState.results}></SearchResults>
</div>
);
};
export const SearchResults = (props) => {
return (
<List>
{props.results?.map((product, index) => (
<ListItem key={index}>
<ListItemText primary={product.name}></ListItemText>
</ListItem>
))}
</List>
);
};
My question is: Why doesn't the SearchResults component get re-rendered when searchState.results changes?
You mutate your state object in your handleChange function. The component doesn't rerender because searchState is still the same object reference from the previous render cycle.
let handleChange = (event) => {
searchState['results'] = searchProducts(event.target.value); // mutation!
searchState['query'] = event.target.value; // mutation!
setSearchState(searchState); // safe reference back into state
};
You shouldn't mutate state object directly. Use a functional state update and shallow copy existing state into a new state object reference. Then update the properties you want to update.
let handleChange = (event) => {
const { value } = event.target;
setSearchState(searchState => ({
...searchState,
results: searchProducts(value),
query: value,
}));
};

Categories

Resources