How to update state of parent component with a lifted up props? - javascript

I have a problem with updating the state of parent component with a props from child component. It seems the following code is not working, however it looks fine
setUsersList(prevState => {
return [...prevState, data];
});
My parent component receives an object. Console.log(data) outputs the object received from child component. However, when console logging updated state (console.log(usersList)) it returns an empty array
Parent component:
import React, { useState } from "react";
import AddUser from "./components/Users/AddUser";
import UsersList from "./components/Users/UsersList";
function App() {
const [usersList, setUsersList] = useState([]);
const addUserHandler = data => {
console.log(data);
setUsersList(prevState => {
return [...prevState, data];
});
console.log(usersList);
};
return (
<div>
<AddUser onAddUser={addUserHandler}></AddUser>
<UsersList users={usersList}></UsersList>
</div>
);
}
export default App;
Child component:
import React, { useState } from "react";
import Button from "../UI/Button";
import Card from "../UI/Card";
import styles from "./AddUser.module.css";
const AddUser = props => {
const [inputData, setInputData] = useState({ name: "", age: "" });
const addUserHandler = event => {
event.preventDefault();
if (
inputData.age.trim().length === 0 ||
inputData.name.trim().length === 0
) {
return;
}
if (+inputData.age < 1) {
return;
}
props.onAddUser(inputData);
console.log(inputData.name, inputData.age);
setInputData({ name: "", age: "" });
};
const usernameChangeHandler = event => {
setInputData({ ...inputData, name: event.target.value });
};
const ageChangeHandler = event => {
setInputData({ ...inputData, age: event.target.value });
};
return (
<Card className={styles.input}>
<form onSubmit={addUserHandler}>
<label htmlFor="username">Username</label>
<input
id="username"
type="text"
onChange={usernameChangeHandler}
value={inputData.name}
></input>
<label htmlFor="age">Age (Years)</label>
<input
id="age"
type="number"
onChange={ageChangeHandler}
value={inputData.age}
></input>
<Button type="submit">Add User</Button>
</form>
</Card>
);
};
export default AddUser;

Due to the way react re-renders components, your console may not log with the expected state change. Instead you can use useEffect for debugging purposes:
parent component
useEffect(() => {
console.log("usersList", usersList);
}, [usersList])
alternatively, having a console.log statement in the body of your functional component should log the correct 'usersList'.
const [usersList, setUsersList] = useState([]);
console.log("usersList", usersList);

The state variable won't change right away when you call setState function from the useState hook. Since it is an asynchronous event.
So you might need to write your code like this to see the right console.log
const addUserHandler = data => {
console.log(data);
setUsersList(prevState => {
const temp = [...prevState, data];
console.log(temp); // like this
return temp;
});
};
If the state is not updating in the UI. Please paste the error or the warning message.

Since setUserList is async function, you can not see the changes on console in the addUserHandler function.
function App() {
const [usersList, setUsersList] = useState([]);
const addUserHandler = data => {
console.log(data);
setUsersList(prevState => {
return [...prevState, data];
});
};
console.log(usersList);
return (
<div>
<AddUser onAddUser={addUserHandler}></AddUser>
<UsersList users={usersList}></UsersList>
</div>
);
}
export default App;
This will work. Thanks.

Related

LocalStorage doesn't set items into itself

I've got a bug with LocalStorage on react.js. I try to set a todo into it, but it doesn't load. This is the code:
import React, { useState, useRef, useEffect } from 'react';
import './App.css';
import TodoList from './TodoList';
const { v4: uuidv4 } = require('uuid');
const LOCAL_STORAGE_KEY = 'todoApp.todos'
function App() {
const [todos, setTodos] = useState([]);
const TodoNameRef = useRef()
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 HandleAddTodo(e){
const name = TodoNameRef.current.value
if (name==='') return
setTodos(prevTodos => {
return[...prevTodos, { id:uuidv4(), name:name, complete:false}]
})
TodoNameRef.current.value = null
}
return (
<>
<TodoList todos={todos}/>
<input ref={TodoNameRef} type="text" />
<button onClick={HandleAddTodo}>Add todo</button>
<button>clear todo</button>
<p>0 left todo</p>
</>
)
}
export default App;
This is TodoList.js
import React from 'react'
import Todo from './Todo';
export default function TodoList({ todos }) {
return (
todos.map(todo =>{
return <Todo key ={todo.id} todo={todo} />
})
)
}
And as last Todo.js:
import React from 'react'
export default function Todo({ todo }) {
return (
<div>
<label>
<input type="checkbox" checked={todo.complete}/>
{todo.name}
</label>
</div>
)
}
What the code has to do is load a todo into the local storage, and after refreshing the page reload it into the document. The code I implemented
I just started with react but I hope anyone can pass me the right code to make it work. If anyone need extra explenation, say it to me.
Kind regards, anonymous
Try to decouple your local storage logic into it's own react hook. That way you can handle getting and setting the state and updating the local storage along the way, and more importantly, reuse it over multiple components.
The example below is way to implement this with a custom hook.
const useLocalStorage = (storageKey, defaultValue = null) => {
const [storage, setStorage] = useState(() => {
const storedData = localStorage.getItem(storageKey);
if (storedData === null) {
return defaultValue;
}
try {
const parsedStoredData = JSON.parse(storedData);
return parsedStoredData;
} catch(error) {
console.error(error);
return defaultValue;
}
});
useEffect(() => {
localStorage.setItem(storageKey, JSON.stringify(storage));
}, [storage]);
return [storage, setStorage];
};
export default useLocalStorage;
And you'll use it just like how you would use a useState hook. (Under the surface it is not really more than a state with some side effects.)
const LOCAL_STORAGE_KEY = 'todoApp.todos'
function App() {
const [todos, setTodos] = useLocalStorage(LOCAL_STORAGE_KEY, []);
const handleAddTodo = event => {
setTodos(prevTodos => {
return[...prevTodos, {
id: uuidv4(),
name,
complete: false
}]
})
};
return (
<button onClick={HandleAddTodo}>Add todo</button>
);
}
You added the getItem and setItem methods of localStorage in two useEffect hooks.
The following code intializes the todo value in localStorage when reloading the page.
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}, [todos])
So you need to set the todo value in HandleAddTodo event.
I edited your code and look forward it will help you.
import React, { useState, useRef, useEffect } from 'react';
import './App.css';
import TodoList from './TodoList';
const { v4: uuidv4 } = require('uuid');
const LOCAL_STORAGE_KEY = 'todoApp.todos'
function App() {
const [todos, setTodos] = useState([]);
const TodoNameRef = useRef()
useEffect(() => {
const storageItem = localStorage.getItem(LOCAL_STORAGE_KEY);
const storedTodos = storageItem ? JSON.parse(storageItem) : [];
if (storedTodos) setTodos(storedTodos)
}, []);
function HandleAddTodo(e){
const name = TodoNameRef.current.value;
if (name==='') return;
const nextTodos = [...todos, { id:uuidv4(), name:name, complete:false}];
setTodos(nextTodos);
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(nextTodos));//replace todos to nextTodos
TodoNameRef.current.value = null
}
return (
<>
<TodoList todos={todos}/>
<input ref={TodoNameRef} type="text" />
<button onClick={HandleAddTodo}>Add todo</button>
<button>clear todo</button>
<p>0 left todo</p>
</>
)
}
export default App;
There is no need of adding the second useEffect.
You can set your local Storage while submitting in the handleTodo function.
Things you need to add or remove :
Remove the Second useEffect.
Modify your handleTodo function :
const nextTodos = [...todos, { id:uuidv4(), name:name,complete:false}];
setTodos(nextTodos);
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(nextTodos));
Note: Make sure you won't pass todos instead of nextTodos as we know setTodos is an async function There might be a chance we are setting a previous copy of todos

Able to get the updated state from redux store but not able to access the objects inside the updated state from the receiver component

I'm creating a simple book list application using react and managing the state using redux. I've only 2 components that deal with the state, the input component dispatches the action and payload, while the output component should get the updated state data. Using the useSelector() hook inside the output component, I can see that the state is updated, however, when I try to access the array of objects and a length property, I get 'cannot read properties of undefined'. Please let me know what did I miss here.
Here is my code for Input Component
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import styles from "./Input.module.css";
const Input = () => {
const dispatch = useDispatch();
const [bookObject, setBookObject] = useState({
bookName: "",
authorName: "",
});
const inputChangeHandler = (e) => {
if (e.target.id === "bookName" || e.target.id === "authorName") {
setBookObject({
bookName: document.getElementById("bookName").value,
authorName: document.getElementById("authorName").value,
});
}
};
const submitHandler = (e) => {
if (bookObject.bookName !== "" && bookObject.authorName !== "") {
dispatch({
type: "GET_INPUT",
payload: {
name: bookObject.bookName,
author: bookObject.authorName,
id: Math.random(),
},
});
setBookObject({ bookName: "", authorName: "" });
} else {
alert("Enter valid Details");
}
e.preventDefault();
};
return (
<form className={styles.form} onSubmit={submitHandler}>
<div>
<label>Book's Name</label>
<input
id="bookName"
type="text"
placeholder="Enter the book's name"
onChange={inputChangeHandler}
value={bookObject.bookName}
/>
</div>
<div>
<label>Author's Name</label>
<input
id="authorName"
type="text"
placeholder="Enter the Author's name"
onChange={inputChangeHandler}
value={bookObject.authorName}
/>
</div>
<button>Submit</button>
</form>
);
};
export default Input;
Here is the code of the Output Component
import React, { Fragment } from "react";
import styles from "./Output.module.css";
import { useSelector } from "react-redux";
const Output = () => {
const outputState = useSelector((state) => state);
const length = outputState.length
const outputObj = outputState.outputObj
return (
<Fragment>
{length !== undefined ? (
<div className={styles.div}>
<h4>Book List</h4>
<ul>
{outputObj.map((book) => (
<li key={book.id}>
{book.name}, written by {book.author}
</li>
))}
</ul>
</div>
) : (
<h4>The Book List is empty</h4>
)}
</Fragment>
);
};
When I console log the outputState, I get a proper object with outputObj array and a length property, but when I try to access outputState.outputObj or outputState.length, I get the error mentioned. I've also tried using two useSelectors each to get the data separately, but in vain.
Here is the code of reducer
import { createStore } from "redux";
const defaultState = {
outputObj: [],
length: undefined,
};
const bookListReducer = (state = defaultState, action) => {
if (action.type === "GET_INPUT") {
state.outputObj = [...state.outputObj, action.payload];
return {
outputObj: state.outputObj,
length: state.outputObj.length,
};
}
};
const store = createStore(bookListReducer);
export default store;
If there was no action, or your action's type was not "GET_INPUT" your reducer will return undefined, therefore the state will be flushed. Update your code as follows.
const bookListReducer = (state = defaultState, action) => {
if (action.type === "GET_INPUT") {
state.outputObj = [...state.outputObj, action.payload];
return {
outputObj: state.outputObj,
length: state.outputObj.length,
};
}
return state; // <- HERE
};

react state is not updating the UI

I have a Form Component where it contains a state that should be updated (on input change) and it looks like this:
import { useState } from 'react';
export const Test = () => {
const [state, setState] = useState({
name: 'khaled',
age: 18
})
const handleInputChange = (e) => {
let stateCopy = state
for(let key in stateCopy) {
if(key === 'name') {
stateCopy[key] = e.target.value;
}
}
setState(stateCopy);
}
return(
<div>
<span>Name</span>
<input onChange={ handleInputChange } />
<span>{state.name}</span>
</div>
)
}
and it imported in the app component
import { Test } from '../../components/Test';
function App() {
return (
<Test />
);
}
export default App;
and whenever i try to change the name inout it not update the ui
To make the input a controlled component, both value and onChange props should be assigned.
<input value={state.name} onChange={handleInputChange} />
handleInputChange function can be improved to make sure that the state is updated immutably:
const handleInputChange = ({ target: { value } }) => {
setState(prevState => ({...prevState, name: value}));
}
This does not work because your "stateCopy" object isn't actually a copy, its the actual state object. you are setting the state to the same object which causes react to think the state didn't change at all.
instead you should copy the state like this
const handleInputChange = (e) => {
let stateCopy = {...state}
state.name = e.target.value
setState(stateCopy);
}
You should also note that unless there is a good reason for your choice of state in my opinion you should use a seperate useState for each element in the state which results in the much simpler
import { useState } from 'react';
export const Test = () => {
const [name, setName] = useState('khalad')
const [age, setAge] = useState(18)
const handleInputChange = (e) => {
setName(e.target.value)
}
return(
<div>
<span>Name</span>
<input onChange={ handleInputChange } />
<span>{state.name}</span>
</div>
)
}
simply do it like this, it will work
const handleInputChange = (e) => {
setState({...state, name: e.target.value})
}

React: Child State isn't updating after Parent state gets Updated

I am beginner in Reactjs. I was building an form application using the same. There I was asked to set value of input field from the server, which can be updated by user i.e. an controlled input component.
I fetched the value in parent state then I passed the value to the child state and from there I set value of input field. Now the problem arises when I update the value in parent state then the value isn't getting updated in the child state.
See the code below -
App.jsx
import { useEffect, useState } from "react";
import { Child } from "./child";
import "./styles.css";
export default function App() {
const [details, setDetails] = useState({});
useEffect(() => {
fetch("https://reqres.in/api/users/2")
.then((res) => res.json())
.then((data) => setDetails(data));
}, []);
useEffect(() => {
console.log("data of details", details?.data);
}, [details]);
return (
<div className="App">
<h1>Testing</h1>
<Child details={details} setDetails={setDetails} val={details?.data} />
</div>
);
}
Child.jsx
import { useState } from "react";
export const Child = ({ details, setDetails, val }) => {
const [value, setValue] = useState({
save: true,
...val
});
const handleChange = (e) => {
setValue({ ...value, email: e.target.value });
};
const handleSave = () => {
setDetails({
...details,
data: { ...details.data, email: value.email }
});
console.log("Data",value);
};
const handleDelete = () => {
setDetails({ ...details, data: { ...details.data, email: "" } });
console.log("Data",value);
};
return (
<div className="cont">
<input type="text" value={value.email} onChange={handleChange} />
{value.save && <button onClick={handleSave}>save</button>}
<button onClick={handleDelete}>Delete</button>
</div>
);
};
Codesandbox Link:
https://codesandbox.io/s/testing-m3mc6?file=/src/child.jsx:0-801
N.B. I have googled for solution I saw one stackoverflow question also but that wasn't helpful for me as I am using functional way of react.
Any other method of accomplishing this would be appreciated.
Try this in child component:
useEffect(()=>{
setValue({
value,
...val
});
}, [val])

How to access the latest state value in the functional component in React

import React, { useState } from "react";
import Child from "./Child";
import "./styles.css";
export default function App() {
let [state, setState] = useState({
value: ""
});
let handleChange = input => {
setState(prevValue => {
return { value: input };
});
console.log(state.value);
};
return (
<div className="App">
<h1>{state.value}</h1>
<Child handleChange={handleChange} value={state.value} />
</div>
);
}
import React from "react";
function Child(props) {
return (
<input
type="text"
placeholder="type..."
onChange={e => {
let newValue = e.target.value;
props.handleChange(newValue);
}}
value={props.value}
/>
);
}
export default Child;
Here I am passing the data from the input field to the parent component. However, while displaying it on the page with the h1 tag, I am able to see the latest state. But while using console.log() the output is the previous state. How do I solve this in the functional React component?
React state updates are asynchronous, i.e. queued up for the next render, so the log is displaying the state value from the current render cycle. You can use an effect to log the value when it updates. This way you log the same state.value as is being rendered, in the same render cycle.
export default function App() {
const [state, setState] = useState({
value: ""
});
useEffect(() => {
console.log(state.value);
}, [state.value]);
let handleChange = input => {
setState(prevValue => {
return { value: input };
});
};
return (
<div className="App">
<h1>{state.value}</h1>
<Child handleChange={handleChange} value={state.value} />
</div>
);
}
Two solution for you:
- use input value in the handleChange function
let handleChange = input => {
setState(prevValue => {
return { value: input };
});
console.log(state.value);
};
use a useEffect on the state
useEffect(()=>{
console.log(state.value)
},[state])
Maybe it is helpful for others I found this way...
I want all updated projects in my state as soon as I added them
so that I use use effect hook like this.
useEffect(() => {
[temp_variable] = projects //projects get from useSelector
let newFormValues = {...data}; //data from useState
newFormValues.Projects = pro; //update my data object
setData(newFormValues); //set data using useState
},[projects])

Categories

Resources