Todos Component:
import React, { useReducer, useState } from 'react';
import Todo2 from './Todo2';
export const ACTIONS = {
ADD_TODO : 'add-todo',
TOGGLE_TODO : 'toggle-todo'
};
function reducer(state, action) {
switch(action.type) {
case ACTIONS.ADD_TODO:
return [...state, {id: Date.now(), name: action.payload.name, complete: false}];
case ACTIONS.TOGGLE_TODO:
const patch = [...state];
console.log('The index is:', action.payload.index);
console.log('The current state is:', patch[action.payload.index].complete);
// update state
patch[action.payload.index].complete = !patch[action.payload.index].complete;
console.log('The updated state is:', patch[action.payload.index].complete);
console.log('The patch is:', patch);
return patch;
}
}
export default function Todos() {
const [todos, dispatch] = useReducer(reducer, []);
const [name, setName] = useState('');
function handleSubmit(e) {
e.preventDefault();
dispatch({type: ACTIONS.ADD_TODO, payload: {name: name}});
setName('');
}
return (
<div>
<form onSubmit={handleSubmit}>
<input type="text" value={name} onChange={e => setName(e.target.value)}/>
</form>
{todos.map((todo, i) => <Todo2 key={todo.id} todo={todo} index={i} dispatch={dispatch} />)}
</div>
);
}
Todo Component:
import React from 'react';
import { ACTIONS } from './Todos2';
export default function Todo2({ todo, dispatch, index }) {
return (
<div>
<span style={{color: todo.complete ? 'green':'red' }}>{todo.name}</span>
<button onClick={e => dispatch({type: ACTIONS.TOGGLE_TODO, payload : {index: index}})}>Toggle</button>
</div>
)
}
I am trying to update an object inside an array, setting its "complete" property to either true or false depending on its current value. I console.logged the results but in the end the patch never gets it's updated data, it always retains it's original value.
If I update the state like this, it works and I don't know why this works but the index way of updating does not.
// update state
patch[action.payload.index].complete = true;
I created a codesandbox example and reproduced the same issue, then I changed const patch = [...state] to:
import _ from 'lodash'
...
...
const patch = _.cloneDeep(state)
And, everything else staying the same, it worked like a charm. Here is the code Now I know that, spread operator ..., does create a shallow copy rather than a deep copy. Therefore, I think your !patch[action.payload.index].complete is updated in the same line, creating a paradoxical assignment (like a double update). Couldn't find a technical reference to explain this better, but the issue is for sure not deep copying the object.
Suggestion
case ACTIONS.TOGGLE_TODO:
return state.map((el, idx) => idx === action.payload.index ? {...el, complete: !el.complete} : el)
Related
i need to store the rating selected in local storage , when the user clicks the button the value in it should get stored in the object rate corresponding to the question id.
import React, { useState ,useEffect} from 'react'
import "./rating_button.css"
function Rating(props) {
let initial={1:'',2:'',3:'',4:'',5:''}
const [rate,setRate]=useState(initial)
const handle=(i)=>{
if(props.id===1)
{setRate({ ...rate, 1:i })
}
else if(props.id===2)
{
setRate({ ...rate, 2:i })
}
else if(props.id===3)
{
setRate({ ...rate, 3:i })
}
else if(props.id===4)
{
setRate({ ...rate, 4:i })
}
else
{
setRate({ ...rate, 2:i })
}
}
const user=props.user;
useEffect(() => {
localStorage.setItem(user, JSON.stringify(rate));
},[]);
const type=props.inputType;
if(type==='text')
{
return (
<>
<textarea value='' onChange={()=>handle(this.value)}/>
</>
)
}
return (
<>
{props.scale.map((i)=>{
return(
<button className='rbtn' onClick={() => handle(i)}>{i}</button>
)
})}
</>
)
}
export default Rating
this code does not seem to work, i'm sometimes getting one key value updated but the state does not seem to persist.i'm trying to store the value in the button clicked into the rate object for 5 different questions.
you can use the spread operator to create a copy of the previous state, update the desired key-value pair, and then set the state to the updated object.
import React, { useState, useEffect } from 'react';
import './rating_button.css';
function Rating(props) {
const [rate, setRate] = useState({ 1: '', 2: '', 3: '', 4: '', 5: '' });
const handle = (i) => {
setRate({ ...rate, [props.id]: i });
};
const user = props.user;
useEffect(() => {
localStorage.setItem(user, JSON.stringify(rate));
}, [rate, user]);
const type = props.inputType;
if (type === 'text') {
return (
<>
<textarea value='' onChange={() => handle(this.value)} />
</>
);
}
return (
<>
{props.scale.map((i) => {
return (
<button className='rbtn' onClick={() => handle(i)}>
{i}
</button>
);
})}
</>
);
}
export default Rating;
the handle function uses the spread operator to create a copy of the rate state, updates the key-value pair corresponding to the props.id value, and then sets the state to the updated object. The useEffect hook now depends on the rate and user variables, so it will be triggered whenever the rate state changes.
Your useEffect is just running first time, when the component mounts but doesn't subsequently updates as the rate object's state changes after each re-render. It is because of empty dependency array in useEffect, change it to:
useEffect(() => {
localStorage.setItem(user, JSON.stringify(rate));
},[rate]);
I am new to react and creating my first react app. not sure why the todo list is not saved even though I have used localStorage set and get methods. I am also getting error about the key in my map method. I can't seen to find any issues on my own with the code.Below is the code of the todo list App
import TodoList from "./TodoList";
import {v4 as uuid} from 'uuid'
function App() {
const [todos,setTodos] = useState([{}]);
const inputRef = useRef();
const LOCAL_STORAGE_KEY = "todoapp"
useEffect(() =>{
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY))
if(storedTodos){
setTodos(storedTodos)}
}, [])
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY,JSON.stringify(todos))
}, [todos])
function toggleTodo(id){
const newTodos= [...todos]
const todo = newTodos.find(todo => todo.id === id)
todo.complete = !todo.complete
setTodos(newTodos)
}
function handleAdd(e) {
const name = inputRef.current.value;
if(name === "")return
setTodos(prevTodos => {
return [...prevTodos,{id:uuid(),name:name,complete:false}]
})
inputRef.current.value = null;
}
function handleClearTodos(){
const newTodos = todos.filter(todo=>!todo.complete)
setTodos(newTodos)
}
return (
<>
<h1>Chores!!</h1>
<TodoList todo={todos} toggleTodo ={toggleTodo} />
<input ref={inputRef} type="text" />
<button onClick ={handleAdd}>Add todo</button>
<button onClick={handleClearTodos}>Clear todo </button>
<div> {todos.filter(todo => !todo.complete).length} left todo</div>
</>
)
}
export default App;
import Todo from './Todo'
export default function TodoList({todo,toggleTodo}) {
return (
todo.map((todo)=> {
return <Todo key={todo.id} todo={todo} toggleTodo={toggleTodo} />
})
)
}
This:
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY,JSON.stringify(todos))
}, [todos])
Is probably taking the initial state of todos on the first render (empty array) and overwriting what data was in their with that initial state.
You might think the previous effect counters this since todos is populated from local storage -- but it doesn't, because on that initial render pass, the second effect will only see the old value of todos. This seems counter-intuitive at first. But it's because whenever you call a set state operation, it doesn't actual change the value of todos immediately, it waits until the render passes, and then it changes for the next render. I.e. it is, in a way, "queued".
For the local storage setItem, you probably want to do it in the event handler of what manipulates the todos and not in an effect. See the React docs.
import TodoList from "./TodoList";
import {v4 as uuid} from 'uuid'
function App() {
const [todos,setTodos] = useState([{}]);
const inputRef = useRef();
const LOCAL_STORAGE_KEY = "todoapp"
const storeTodos = (todos) => {
localStorage.setItem(LOCAL_STORAGE_KEY,JSON.stringify(todos))
setTodos(todos)
}
useEffect(() =>{
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY))
if(storedTodos){
setTodos(storedTodos)}
}, [])
function toggleTodo(id){
const newTodos= [...todos]
const todo = newTodos.find(todo => todo.id === id)
todo.complete = !todo.complete
storeTodos(newTodos)
}
function handleAdd(e) {
const name = inputRef.current.value;
if(name === "")return
storeTodos(prevTodos => {
return [...prevTodos,{id:uuid(),name:name,complete:false}]
})
inputRef.current.value = null;
}
function handleClearTodos(){
const newTodos = todos.filter(todo=>!todo.complete)
storeTodos(newTodos)
}
return (
<>
<h1>Chores!!</h1>
<TodoList todo={todos} toggleTodo ={toggleTodo} />
<input ref={inputRef} type="text" />
<button onClick ={handleAdd}>Add todo</button>
<button onClick={handleClearTodos}>Clear todo </button>
<div> {todos.filter(todo => !todo.complete).length} left todo</div>
</>
)
}
export default App;
As the for the key error, we'd need to see the code in TodoList, but you need to ensure when you map over them, that the id property of each todo is passed to a key prop on the top most element/component within the map callback.
After the first render, the useReducer hook doesn't react to changes in its initialArg (second positional) argument. It makes it hard to properly sync it with an external value, without having to rely on an extra cycle by dispatching a reset action inside a useEffect hook.
I built a minimal example. It's a simple, formik-like, form provider. Here's what it looks like:
// App.js
const users = {
1: {
firstName: 'Paul',
lastName: 'Atreides',
},
2: {
firstName: 'Duncan',
lastName: 'Idaho',
},
};
const App = () => {
const [id, setId] = useState(1);
return (
<>
<div>Pick User</div>
<button onClick={() => { setId(1); }} type="button">User 1</button>
<button onClick={() => { setId(2); }} type="button">User 2</button>
<FormProvider initialValues={users[id]}>
<Editor />
</FormProvider>
</>
);
};
// FormProvider.js
const reducer = (state, action) => {
switch (action.type) {
case 'UPDATE_FIELD':
return { ...state, [action.field]: action.value };
default:
throw new Error();
}
};
const FormProvider = ({ children, initialValues }) => {
const [values, dispatch] = useReducer(reducer, initialValues);
const handleChange = useCallback((evt) => {
dispatch({
field: evt.target.name,
type: 'UPDATE_FIELD',
value: evt.target.value,
});
}, []);
return (
<FormContext.Provider value={{ handleChange, values }}>
{children}
</FormContext.Provider>
);
};
// Editor.js
const Editor = () => {
const { handleChange, values } = useContext(FormContext);
return (
<>
<div>First name:</div>
<input
name="firstName"
onChange={handleChange}
value={values.firstName}
/>
<div>First name:</div>
<input
name="lastName"
onChange={handleChange}
value={values.lastName}
/>
</>
);
};
If you open the demo and click on the User 2 button, you'll notice that nothing happens. It's not surprising since we know that the useReducer hook gets initialised once using the provided initialArg argument and never reads its value again.
What I expect is the useReducer state to reflect the new initialArg prop, i.e. I want to see "Duncan" in the First name input after clicking on the User 2 button.
From my point of vue, I can see two options:
1. Passing a key prop to the FormProvider component.
// App.js
const App = () => {
// ...
return (
<>
{/* ... */}
<FormProvider key={id} initialValues={users[id]}>
<Editor />
</FormProvider>
</>
);
};
This will indeed fix the problem by destroying and re-creating the FormProvider component (and its children) every time the id changes. But it feels like a hack to me. Plus, it seems inefficient to rebuild that entire part of the tree (which is substantial in the real application) just to get that input values updated. However, this seems to be a common fix for such problems.
2. Dispatch a RESET action whenever initialValues changes
// FormProvider.js
const reducer = (state, action) => {
switch (action.type) {
case 'UPDATE_FIELD':
return { ...state, [action.field]: action.value };
case 'RESET':
return action.values;
default:
throw new Error();
}
};
const FormProvider = ({ children, initialValues }) => {
// ...
const isFirstRenderRef = useRef(true);
useEffect(() => {
if (!isFirstRenderRef.current) {
dispatch({
type: 'RESET',
values: initialValues,
});
}
}, [initialValues]);
useEffect(() => {
isFirstRenderRef.current = false;
}, []);
// ...
};
This will work as well, but, because it's happening inside a useEffect hook, it will require an extra cycle. It means that there'll be a moment where the form will contain stale values. If the user types at that moment, it could cause a race condition.
3. Idea
I read in this article by Mark Erikson that:
Function components may call setSomeState() directly while rendering, as long as it's done conditionally and isn't going to execute every time this component renders. [...] If a function component queues a state update while rendering, React will immediately apply the state update and synchronously re-render that one component before moving onwards.
So it seems that I should be able to call dispatch({ type: RESET, values: initialValues }); directly from the body of the function, under the condition that initialValues did change (I'd use a ref to keep track of its previous value). This should result in the state being updated in just one cycle. However, I couldn't get this to work.
——
What do you think is best between option 1, 2 and (3). Any advice/guidance on how I should address this problem?
I am dynamically adding <div> elements to a component by adding them to an array. This is not a problem and works well. The issue I'm trying to solve here is removing the <div> on double click by passing the id of the <div> that was doubled clicked with props when the reducer is dispatched.
The main issue is the array filter function only works when I code hard the div id both on the div and in the filter function when I want to pass the id of e.target.id on dispatch of delDiv reducer.
Note: I can remove the div successfully by changing the addDivReducer like this:
case "ADD_DIV":
return state.concat(
<DivComponent
key={Math.floor(Math.random() * 100) + 1}
id={11} ***************************************************** Changed
/>
);
case "DELETE_DIV":
state = state.filter((elements) => {
return elements.props.id !== 11; *********************************** Changed
});
return state;
But the desired effect is to pass id as props on dispatch as seen in my code below
The reducer that adds a removes elements look like this:
import DivComponent from "../../components/AddDivComponent";
const addDivReducer = (state, action) => {
switch (action.type) {
case "ADD_DIV":
return state.concat(
<DivComponent
key={Math.floor(Math.random() * 100) + 1}
id={Math.floor(Math.random() * 100) + 1}
/>
);
case "DELETE_DIV":
state = state.filter((elements) => {
return elements.props.id !== action.payload;
});
return state;
default:
return (state = []);
}
};
export default addClipartReducer;
The actions index.js look like:
export const addDiv = (props) => {
return {
type: "ADD_DIV",
payload: props,
};
};
export const deleteDiv = (props) => {
return {
type: "DELETE_DIV",
payload: props,
};
};
The delete reducer is being dispatched when the div is double clicked on like this in AddDivComponent.js:
import { useDispatch } from "react-redux";
import { deleteDiv } from "../../store/actions";
const AddDivComponent = (props) => {
const dispatch = useDispatch();
const removeClipart = (e) => {
dispatch(deleteDiv(e.target.id));
};
return(
<div
id={props.id}
className="my-div"
onDoubleClick={removeDiv}
/>
);
};
export default DivComponent;
Finally the array of <div> elements is being shown here in Canvas.js:
import { useSelector } from "react-redux";
const Canvas = () => {
const divList = useSelector((state) => state.addDIV);
return(
<div className="canvas">
{divList}
</div>
);
};
export default Canvas;
you are mutating state at your DELETE_DIV reducer. If you need to handle state, create a copy a first:
// mutating state here to a new value, can lead to problems
state = state.filter((elements) => {
return elements.props.id !== action.payload;
});
I would suggest to return filter directly, given filter already returns the desired next state, while not mutating the original:
case "DELETE_DIV":
return state.filter((elements) => {
return elements.props.id !== action.payload;
});
// EditableNote.js
function EditableNote({ note }) {
const [editableNote, setEditableNote] = useState(note);
const { title, content } = editableNote;
const dispatch = useDispatch();
useEffect(() => {
setEditableNote(note);
}, [note]);
›
useEffect(() => {
dispatch(saveEditableNote(editableNote)); // I think here is problem
}, [dispatch, editableNote]);
const handleBlur = e => {
const name = e.target.id;
const value = e.currentTarget.textContent;
setEditableNote({ ...editableNote, [name]: value });
};
return (
<EditNote spellCheck="true">
<NoteTitle
id="title"
placeholder="Title"
onBlur={handleBlur}
contentEditable
suppressContentEditableWarning={true}>
{title}
</NoteTitle>
<NoteContent
id="content"
placeholder="Note"
onBlur={handleBlur}
contentEditable
suppressContentEditableWarning={true}>
{content}
</NoteContent>
</EditNote>
);
}
export default EditableNote;
I have EditableNote component which is contentEditable. I set its initial state through props from its parent(Note). So if something is changed in note, then editableNote has to changed.
To keep recent props state, I use useEffect. Everything seems working well.
Here is an issue. If I first change color of note and typing, it is updated as expected. But on contrast, if I first typing and change color, editableNote state is not updated.
// Reducer.js
case actions.GET_NOTE_COLOR:
return {
...state,
bgColor: action.payload
}
case actions.CHANGE_NOTE_COLOR:
return {
...state,
notes: state.notes.map(note => note.id === action.payload ?
{ ...note, bgColor: state.bgColor }
: note
)
};
case actions.SAVE_EDITABLE_NOTE: // payload is old value
return {
...state,
editableNote: action.payload,
}
I check what happened in an action. I found everything works until CHANGE_NOTE_COLOR but when dispatch SAVE_EDITABLE_NOTE, its payload is not updated!
I have no idea.. plz.. help me...TT
You have to use the connect wrapper provided by redux to connect actions and state of redux to your components
https://react-redux.js.org/api/connect