React Multiple Checkboxes are not visible as selected after change - reactjs

I have a list of checkboxes.
Checkbox is not visible as selected even after the value has been changed.
Below is my code: -
import React, { useState } from "react";
import { render } from "react-dom";
const CheckboxComponent = () => {
const [checkedList, setCheckedList] = useState([
{ id: 1, label: "First", isCheck: false },
{ id: 2, label: "Second", isCheck: true }
]);
const handleCheck = (e, index) => {
checkedList[index]["isCheck"] = e.target.checked;
setCheckedList(checkedList);
console.log(checkedList);
};
return (
<div className="container">
{checkedList.map((c, index) => (
<div>
<input
id={c.id}
type="checkbox"
checked={c.isCheck}
onChange={e => handleCheck(e, index)}
/>
<label htmlFor={c.id}>{c.label}</label>
</div>
))}
</div>
);
};
render(<CheckboxComponent />, document.getElementById("root"));
I was working fine for a simple checkbox outside the loop.
I am not sure where is the problem.
Here is the link - https://codesandbox.io/s/react-multiple-checkboxes-sczhy?file=/src/index.js:0-848

Cause you pass an array to the state, so if you want your react component re-render, you must let the react know that your state change. On your handleCheck, you only change property of an value in that array so the reference is not changed.
The handleCheck function should be look like this
const handleCheck = (e, index) => {
const newCheckList = [...checkedList];
newCheckList[index]["isCheck"] = e.target.checked;
setCheckedList(newCheckList);
};

Do this instead:
const handleCheck = (e, index) => {
setCheckedList(prevState => {
const nextState = prevState.slice()
nextState[index]["isCheck"] = e.target.checked;
return nextState
});
};
Since checkedList is an array, (considered as object and handled as such), changing a property won't change the array itself. So React can know that something changed.

Related

How to ensure useState works when mocking custom react hook

I have a component which imports a custom hook. I want to mock returned values of this hook but ensure the useState still works when I fire and event.
component.tsx
export const Component = () => {
const { expanded, text, handleClick, listOfCards } = useComponent();
return (
<div>
<button id="component" aria-controls="content" aria-expanded={expanded}>
{text}
</button>
{expanded && (
<div role="region" aria-labelledby="component" id="content">
{listOfCards.map((card) => (
<p>{card.name}</p>
))}
</div>
)}
</div>
);
};
useComponent.tsx
const useComponent = () => {
const [expanded, setExpanded] = useState(false);
const { listOfCards } = useAnotherCustomHook();
const { translate } = useTranslationTool();
return {
text: translate("id123"),
expanded,
handleClick: () => setExpanded(!expanded),
listOfCards,
};
};
component.test.tsx
jest.mock("./component.hook");
const mockuseComponent = useComponent as jest.Mock<any>;
test("Checks correct attributes are used, and onClick is called when button is clicked", () => {
mockuseComponent.mockImplementation(() => ({
text: "Click to expand",
listOfCards: [{ name: "name1" }, { name: "name2" }],
}));
render(<Component />);
const button = screen.getByRole("button", { name: "Click to expand" });
expect(button).toHaveAttribute('aria-expanded', 'false');
fireEvent.click(button);
expect(button).toHaveAttribute('aria-expanded', 'true');
});
With the above test aria-expanded doesnt get set to true after we fire the event because im mocking the whole hook. So my question is, is there a way to only mock part of the hook and keep the useState functionality?

Change a checkbox checked state from a useEffect hook in React?

The value of context.number is 1. I want to set the input checked value to true based off the context.number. So if context.number is 1, the checkbox is checked for Module 1, showing that Module 1 is completed. I can only change the input value from the onChange event though, so i assume i need a useEffect hook so it does it automatically when context.number changes?
import React, { useState, useContext, useEffect } from "react";
import { Link } from "react-router-dom";
import { AppContext } from "../context/AppContext";
export default function Menu() {
const context = useContext(AppContext);
//Determine the modules
const modules = [
{
title: `Introduction`,
subtitle: "Lesson 1",
},
{
title: `Overview`,
subtitle: "Lesson 2",
}
];
//Create a checked state for each module
const [checkedState, setCheckedState] = useState(new Array(modules.length).fill(false));
//Change checked value method
const handleOnChange = (position) => {
//map through checked states array, if position === mapped item index, flip the value
const updatedCheckedState = checkedState.map((item, index) => (index === position ?
!item : item));
//set that items state to the new value in the array
setCheckedState(updatedCheckedState);
};
return (
<>
<div>
{modules.map((module, index) => (
<div className={styles.menuCard}>
<Link key={index} to={`/Module/${index + 1}`}>
<h2>{module.title}</h2>
<p>{module.subtitle}</p>
</Link>
<input
id={`check${index}`}
type="checkbox"
onChange={() => handleOnChange(index)}
checked={checkedState[index]}
/>
</div>
))}
</div>
</>
);
}
I put this is my context file and was able to get it to work. This is a poorly worded question I realize but found my answer none the less. useEffect wasn't the problem, it was that each checkbox was only saving local state so if i rendered another page the checkboxes went back to being unchecked.
import React, { createContext, useState } from "react";
const AppContext = createContext();
function AppProvider(props) {
//Determine the modules
const modules = [
{
title: `Introduction`,
subtitle: "Lesson 1",
},
{
title: `Overview`,
subtitle: "Lesson 2",
}
];
//set module number state
const [number, setNumber] = useState();
//Create a checked state for each module
const [checkedState, setCheckedState] = useState(new Array(modules.length).fill(false));
//change module number method
function completionHandler(value) {
setNumber(value);
}
//Change checked value method
function handleChange(position) {
const updatedCheckedState = checkedState.map((item, index) => (index == position ? !item : item));
setCheckedState(updatedCheckedState);
}
//export method and state value
const value = {
number: number,
modules: modules,
checkedState: checkedState,
handleChange: handleChange,
completionHandler: completionHandler,
};
return <AppContext.Provider value={value}>{props.children}</AppContext.Provider>;
}
export { AppContext, AppProvider };

Preventing unnecessary rendering with React.js

I have a very basic application to test how to prevent unnecessary rendering, but I'm very confused as it is not working no matter what I try. Please take a look.
App.js
import { useState, useCallback } from "react";
import User from "./User";
let lastId = 0;
function App() {
const [users, setUsers] = useState([
{ id: 0, name: "Nicole Kidman", gender: "Female" },
]);
const handleUserChange = useCallback(
(e, userId) => {
const { name, value } = e.target;
const newUsers = [...users];
const index = newUsers.findIndex((user) => user.id === userId);
if (index >= 0) {
newUsers[index] = {
...newUsers[index],
[name]: value,
};
setUsers(newUsers);
}
},
[users]
);
const addNewUser = useCallback(() => {
let newUser = { id: ++lastId, name: "John Doe", gender: "Male" };
setUsers((prevUsers) => [...prevUsers, newUser]);
}, []);
return (
<div className="App">
<button onClick={addNewUser}>Add user</button>
<br />
{users.map((user) => (
<User key={user.id} user={user} handleUserChange={handleUserChange} />
))}
</div>
);
}
export default App;
User.js
import { useRef, memo } from "react";
const User = memo(({ user, handleUserChange }) => {
const renderNum = useRef(0);
return (
<div className="user">
<div> Rendered: {renderNum.current++} times</div>
<div>ID: {user.id}</div>
<div>
Name:{" "}
<input
name="name"
value={user.name}
onChange={(e) => handleUserChange(e, user.id)}
/>
</div>
<div>
Gender:{" "}
<input
name="gender"
value={user.gender}
onChange={(e) => handleUserChange(e, user.id)}
/>
</div>
<br />
</div>
);
});
export default User;
Why the useCallback and memo doesn't do the job here? How can I make it work, prevent rendering of other User components if another User component is changing(typing something in Input)?
Thank you.
useCallback and useMemo take a dependency array. If any of the values inside that array changes, React will re-create the memo-ized value/callback.
With this in mind, we see that your handleUsersChange useCallback is recreated every time the array users changes. Since you update the users state inside the callback, every time you call handleUsersChange, the callback is re-created, and therefore the child is re-rendered.
Solution:
Don't put users in the dependency array. You can instead access the users value inside the handleUsersChange callback by providing a callback to setUsers functions, like so:
const handleUserChange = useCallback(
(e, userId) => {
const { name, value } = e.target;
setUsers((oldUsers) => {
const newUsers = [...oldUsers];
const index = newUsers.findIndex((user) => user.id === userId);
if (index >= 0) {
newUsers[index] = {
...newUsers[index],
[name]: value,
};
return newUsers;
}
return oldUsers;
})
},
[]
);

React Functional Component - Binding handler to an array

I am trying to bind a handler to an array position that prints the state of the object. The function is being called but the state is always in the initial state.
import React from "react";
export default function MyForm() {
const [state, setState] = React.useState({
buttons: [],
object: null
});
const stateRef = React.useRef(null);
const onExecButtonHandler = (index) => {
console.log("[MyForm][execButton]: " + index);
alert(JSON.stringify(state.object));
alert(JSON.stringify(stateRef.current.object));
};
const onChangeHandler = (event)=>{
let obj = state.object
obj['description'] = event.target.value
console.log(obj)
setState((state) => ({ ...state, object: obj }));
stateRef.current = state;
}
const generateButtons = () => {
let buttons = [];
//buttons.push([name,(event)=>onExecButtonHandler(event,i),obj[i].icon,obj[i].jsFunction])
buttons.push([
"Show Hi",
onExecButtonHandler.bind(this, 1),
"icon",
"custom function"
]);
return buttons;
};
React.useEffect(() => {
console.log("[MyForm][useEffect]");
let buttons = generateButtons();
setState({ buttons: buttons, object: { description: "hi" } });
stateRef.current = state;
}, []);
return (
<>
{state.object && (
<form>
<input text="text" onChange={onChangeHandler} value={state.object.description} />
<input
type="button"
value="Click me"
onClick={() => {
state.buttons[0][1]();
}}
/>
</form>
)}
</>
);
}
You can test here: https://codesandbox.io/s/charming-robinson-g1yuo?fontsize=14&hidenavigation=1&theme=dark
I was able to access the latest state of the object using the "useRef" hook. I'd like to access using the state though. Is there some way to do so?

ReactJs managing an array of stateful components

I have a parent component that I want to use to dynamically add and remove an stateful component, but something weird is happening.
This is my parent component and how I control my list of FilterBlock
import React from 'react'
import FilterBlock from './components/FilterBlock'
export default function List() {
const [filterBlockList, setFilterList] = useState([FilterBlock])
const addFilterBlock = () => {
setFilterList([...filterBlockList, FilterBlock])
}
const onDeleteFilterBlock = (index) => {
const filteredList = filterBlockList.filter((block, i) => i !== index);
setFilterList(filteredList)
}
return (
<div>
{
filterBlockList.map((FilterBlock, index) => (
<FilterBlock
key={index}
onDeleteFilter={() => onDeleteFilterBlock(index)}
/>
))
}
<button onClick={addFilterBlock}></button>
</div>
)
}
You can assume FilterBlock is a stateful react hooks component.
My issue is whenever I trigger the onDeleteFilter from inside any of the added components, only the last pushed FilterBlock gets removed from the list even though I remove it based on its index on the list.
Please check my example here:
https://jsfiddle.net/emad2710/wzh5Lqj9/10/
The problem is you're storing component function FilterBlock to the state and mutate the parent state inside a child component.
You may know that mutating state directly will cause unexpected bugs. And you're mutating states here:
/* Child component */
function FilterBlock(props) {
const [value, setValue] = React.useState('')
const options = [
{ value: 'chocolate', label: 'Chocolate' },
{ value: 'strawberry', label: 'Strawberry' },
{ value: 'vanilla', label: 'Vanilla' }
]
const onInputChange = (e) => {
// child state has been changed, but parent state
// does not know about it
// thus, parent state has been mutated
setValue(e.target.value)
}
return (
<div>
<button onClick={props.onDeleteFilter}>remove</button>
<input value={value} onChange={onInputChange}></input>
</div>
)
}
/* Parent component */
const addFilterBlock = () => {
setFilterList([...filterBlockList, FilterBlock])
}
filterBlockList.map((FilterBlock, index) => (
<FilterBlock
key={index}
onDeleteFilter={() => onDeleteFilterBlock(index)}
/>
))
When a FilterBlock value changes, FilterBlock will store its state somewhere List component cannot control. That means filterBlockList state has been mutated. This leads to unpredictable bugs like the above problem.
To overcome this problem, you need to manage the whole state in parent component:
const {useCallback, useState} = React;
function FilterBlock(props) {
const options = [
{ value: 'chocolate', label: 'Chocolate' },
{ value: 'strawberry', label: 'Strawberry' },
{ value: 'vanilla', label: 'Vanilla' }
]
return (
<div>
<button onClick={props.onDeleteFilter}>remove</button>
<input value={props.value} onChange={props.onChange}></input>
</div>
)
}
function App() {
const [values, setValues] = useState(['strawberry']);
const addFilterBlock = useCallback(() => {
setValues(values.concat(''));
}, [values]);
const onDeleteFilterBlock = useCallback((index) => {
setValues(values.filter((v, i) => i !== index));
}, [values]);
const setFilterValue = useCallback((index, value) => {
const newValues = [...values];
newValues[index] = value;
setValues(newValues);
}, [values]);
return (
<div>
{
values.map((value, index) => (
<FilterBlock
key={index}
onDeleteFilter={() => onDeleteFilterBlock(index)}
value={value}
onChange={(e) => setFilterValue(index, e.target.value)}
/>
))
}
<button onClick={addFilterBlock}>add</button>
</div>
)
}
ReactDOM.render(<App />, document.querySelector("#app"))
JSFiddle

Resources