I Try not to Rerender Persons Component When ShowCockpit State Changes In MainAssignment Component. Like when i do in Cockpit Component, it doesn't rerender When Persons state change.
In This Case We Have 3 Components MainAssignment Component [parnt] , Cockpit Component [child] , Persons Component [child].
/********************************************************/
/*** MainAssignment Component ***/
import React, { useCallback, useState } from 'react';
import Persons from './persons';
import Coockpit from './cockpit';
const MainAssignment = () => {
// All State
const [persons, setPersons] = useState([
{ id: '1', name: 'mustafa', age: 24 },
{ id: '2', name: 'ahmed', age: 25 },
{ id: '3', name: 'saad', age: 26 },
]);
const [showPersons, setShowPersons] = useState(true);
const [showCoockpit, setShowCoockpit] = useState(true);
const togglePersonHandler = useCallback(() => {
setShowPersons(!showPersons);
}, [showPersons]);
// change name in specific object in persons state
const nameChangeHandler = (e, id, personIndex) => {
let newPersons = [...persons];
let person = { ...newPersons[personIndex] };
person.name = e.target.value;
newPersons[personIndex] = person;
setPersons(newPersons);
};
// delete object from persons state
const deletePersonHandler = (personIndex) => {
let newPersons = [...persons];
newPersons.splice(personIndex, 1);
setPersons(newPersons);
};
// Main Render
return (
<>
<button
onClick={() => {
setShowCoockpit((prev) => !prev);
}}
>
remove Coockpit
</button>
{showCoockpit ? (
<div style={{ border: '1px solid' }}>
<Coockpit clicked={togglePersonHandler} personsLength={persons.length} showPersons={showPersons} />
</div>
) : null}
{showPersons ? <Persons persons={persons} clicked={deletePersonHandler} changed={nameChangeHandler} /> : null}
</>
);
};
export default MainAssignment;
/********************************************************/
/*** Cockpit Component ***/
/********************************************************/
/*** Cockpit Component ***/
import React, { useRef } from 'react';
const Cockpit = ({ clicked }) => {
let toggleBtnRef = useRef(null);
console.log('render => Cockpit');
return (
<div>
<h1>hi i'm a main assin from cockpit</h1>
<button className="toggle-persons" onClick={clicked} ref={toggleBtnRef}>
toggle persons
</button>
</div>
);
};
// in Cockpit i use React.memo and it work
export default React.memo(Cockpit);
/********************************************************/
/*** Persons Component ***/
import React, { useEffect, useRef } from 'react';
import Person from './person';
const Persons = ({ persons, clicked, changed }) => {
console.log('render => personssss');
const mainRef = {
allInputPersonRef: useRef([]),
};
return (
<>
{persons?.map((person, idx) => (
<Person
key={idx}
name={person.name}
age={person.age}
position={idx}
index={idx}
ref={mainRef}
click={() => {
clicked(idx);
}}
changed={(e) => {
changed(e, person.id, idx);
}}
/>
))}
</>
);
};
// in Persons i use React.memo and it doesn't work
export default React.memo(Persons);
/********************************************************/
/*** Person Component ***/
import React from 'react';
const Person = React.forwardRef((props, ref) => {
const { allInputPersonRef } = ref;
// value of props
const { name, age, click, changed, children, index } = props;
return (
<div>
<p onClick={click}>
i'm {name} and i'm {age} years old
</p>
<p> i'am props children: {children}</p>
<input type="text" onChange={changed} value={name} ref={(el) => (allInputPersonRef.current[index] = el)} />
<button onClick={click}>delete this person</button>
</div>
);
});
export default Person;
React.memo can prevent children from rerendering when the parent component rerenders.
It compares (by reference) each previous and next prop. When one of them is different React will rerender the child normally.
In your case you are always passing new function to changed prop
const nameChangeHandler = (e, personIndex) => {
let newPersons = [...persons];
let person = { ...newPersons[personIndex] };
person.name = e.target.value;
newPersons[personIndex] = person;
setPersons(newPersons);
};
How to avoid this?
Make sure that nameChangeHandler is the same function each time you need to rerender and you don't want to rerender the Person component. https://reactjs.org/docs/hooks-reference.html#usecallback
const nameChangeHandler = useCallback((e, personIndex) => {
setPersons((persons) => {
let newPersons = [...persons];
let person = { ...newPersons[personIndex] };
person.name = e.target.value;
newPersons[personIndex] = person;
return newPersons
});
}, []);
Similarly you should memorize deletePersonHandler function
const deletePersonHandler = useCallback((personIndex) => {
setPersons((persons)=>{
let newPersons = [...persons];
newPersons.splice(personIndex, 1);
return newPersons
});
}, []);
using useCallback with togglePersonHandler and deletePersonHandler
const nameChangeHandler = useCallback((e, id, personIndex) => {
let newPersons = [...persons];
let person = { ...newPersons[personIndex] };
person.name = e.target.value;
newPersons[personIndex] = person;
setPersons(newPersons);
}, []);
const deletePersonHandler = useCallback((personIndex) => {
let newPersons = [...persons];
newPersons.splice(personIndex, 1);
setPersons(newPersons);
}, []);
Related
I have a simple app, sorta for chat purpuses. I fetch data from static file in json format. So this app shows all the messages from that file but also I want to edit the messeges, delete them and add via local storage. For that I used useEffect, but after refresh all the changes I do disappear.
This is my component:
export const WorkChat = (props) => {
const [messageValue, setMessageValue] = useState('');
const [edit, setEdit] = useState(null);
const [editmessageValue, setMessageEditValue] = useState('')
const submitMessage = () => {
const newMessage = {
id: Math.floor(Math.random() * 10000),
message: messageValue
}
props.addMessage(newMessage);
setMessageValue('')
}
const removeMsg = (id) => {
props.deleteMessage(id)
}
const goToEditMode = (message) => {
setEdit(message.id);
setMessageEditValue(message.message)
}
const saveChanges = (id) => {
const newMessagesArray = props.messages.map(m => {
if(m.id === id){
m.message = editmessageValue
}
return m
})
props.updateMessage(newMessagesArray);
setEdit(null)
}
useEffect(()=> {
let data = localStorage.getItem('work-messages');
if(data){
props.setMessages(JSON.parse(data))
}
}, []);
useEffect(()=> {
localStorage.setItem('work-messages', JSON.stringify(props.messages))
},[props.messages])
return (
<div className={s.workChatContainer}>
<input className={s.workInput} placeholder='Enter work message...' onChange={(e)=> setMessageValue(e.target.value)} value={messageValue}/>
<button className={`${s.btn} ${s.sendBtn}`} onClick={()=>submitMessage()}><SendIcon style={{fontSize: 20}}/></button>
<div>
{props.messages.map(m => (
<div key={m.id} className={s.messages}>
{edit !== m.id ? <div>
<span className={s.message}>{m.message}</span>
<button className={`${s.btn} ${s.deleteBtn}`} onClick={()=> removeMsg(m.id)}><DeleteOutlineIcon style={{fontSize: 15}}/></button>
<button className={`${s.btn} ${s.editBtn}`} onClick={()=> goToEditMode(m)}><EditIcon style={{fontSize: 15}}/></button>
</div>
:
<form>
<input className={s.editInput} value={editmessageValue} onChange={(e)=> setMessageEditValue(e.target.value)}/>
<button className={`${s.btn} ${s.saveBtn}`} onClick={()=> saveChanges(m.id)}><BeenhereIcon style={{fontSize: 15}}/></button>
</form>
}
</div>
))}
</div>
</div>
)
}
Just in case, this is my container component:
import { connect } from "react-redux"
import { setFloodMessagesAC, addFloodMessageAC, deleteFloodMessageAC, upadateMessageAC } from "../../redux/flood-reducer"
import { FloodChat } from "./FloodChat"
import { useEffect } from 'react'
import data from '../../StaticState/dataForFlood.json'
const FloodChatApiContainer = (props) => {
useEffect(()=> {
props.setFloodMessages(data)
}, [])
return <FloodChat messages={props.messages}
setFloodMessages={props.setFloodMessages}
addFloodMessage={props.addFloodMessage}
deleteFloodMessage={props.deleteFloodMessage}
upadateMessage={props.upadateMessage}
/>
}
const mapStateToProps = (state) => ({
messages: state.flood.messages
})
export const FloodChatContainer = connect(mapStateToProps, {
setFloodMessages: setFloodMessagesAC,
addFloodMessage: addFloodMessageAC,
deleteFloodMessage: deleteFloodMessageAC,
upadateMessage: upadateMessageAC
})(FloodChatApiContainer)
Why useEffect doesn't work? It seems to me like it should, but it doesnt.
I figured it out. Since I use data from static file, I need to implement functions that get/set data from/to local storage right where I import it which is container component. Once I put those useEffect functions in container component it works perfectly well.
const FloodChatApiContainer = (props) => {
useEffect(()=> {
props.setFloodMessages(data)
}, [])
useEffect(()=> {
let data = JSON.parse(localStorage.getItem('flood-messages'));
if(data){
props.setFloodMessages(data)
}
console.log('get')
}, [])
useEffect(() => {
localStorage.setItem('flood-messages', JSON.stringify(props.messages));
console.log('set')
}, [props.messages]);
return <FloodChat messages={props.messages}
setFloodMessages={props.setFloodMessages}
addFloodMessage={props.addFloodMessage}
deleteFloodMessage={props.deleteFloodMessage}
upadateMessage={props.upadateMessage}
/>
}
const mapStateToProps = (state) => ({
messages: state.flood.messages
})
export const FloodChatContainer = connect(mapStateToProps, {
setFloodMessages: setFloodMessagesAC,
addFloodMessage: addFloodMessageAC,
deleteFloodMessage: deleteFloodMessageAC,
upadateMessage: upadateMessageAC
})(FloodChatApiContainer)
I'm basically trying to call a function (getValue) from a class (Time) in a different file, but there is some issues.
Here is the code for the two files:
Time.js
export default class Time extends Component {
constructor(props) {
super(props);
this.state = {
input: '',
input2: '',
checked: false
}
this.getValue = this.getValue.bind(this);
}
hrChange = e => {
this.setState({input: e.target.value}, function () {this.getValue()})
}
minChange = e => {
this.setState({input2: e.target.value}, function () {this.getValue()})
}
amPm = () => {
this.setState({checked: !this.state.checked}, function () {this.getValue()})
}
getValue = () => {
const list = [
this.state.input,
this.state.input2,
this.state.checked
]
return (list)
}
render() {
return(
<text>some stuff</text>
)
}
}
NewStorage.js
function NewStorage() {
const time = () => {
var obj = new Time();
var list = obj.getValue()
const
hrInput = list[0],
minInput = list[1],
pm = list[2]
return(
console.log(hrInput, minInput, pm, list)
)
return(
time()
)
}
export default NewLocalStorage;
The main issue isn't that I can't call the function, it is that when I call the function, the values of input, input2, and checked are all the original value ('', '', false), not the updated versions (ex: '11', '30', true).
I'm not sure on how to solve this issue.
Your inclusion of the react-hooks tag suggest your hunch that hooks are applicable to solving your problem. I would agree -
const { useState, useEffect } = React
function Time ({ hour, minute, onChange }) {
const [h,setHour] = useState(hour)
const [m,setMinute] = useState(minute)
useEffect(_ => onChange({ hour: h, minute: m }), [h, m])
return <div>
<input value={h} onChange={event => setHour(event.target.value)} />
<input value={m} onChange={event => setMinute(event.target.value)} />
</div>
}
ReactDOM.render(<Time onChange={console.log} />, document.querySelector("main"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<main></main>
In a more sophisticated example, we can use the Time component's onChange callback to update nested state in a parent component, MyForm -
const { useState, useEffect, useCallback } = React
function Time ({ hour = 0, minute = 0, onChange }) {
const [h,setHour] = useState(hour)
const [m,setMinute] = useState(minute)
useEffect(_ => onChange({ hour: h, minute: m }), [h, m, onChange])
return <div>
<input value={h} onChange={event => setHour(event.target.value)} />
<input value={m} onChange={event => setMinute(event.target.value)} />
</div>
}
function MyForm () {
const [data, setData] = useState({ time: { hour: 5, minute: 30 }, foo: "bar" })
const onTimeChange = useCallback(t => setData({ ...data, time: t }), [])
return <form>
<Time hour={data.time.hour} minute={data.time.minute} onChange={onTimeChange} />
<pre>{JSON.stringify(data, null, 2)}</pre>
</form>
}
ReactDOM.render(<MyForm />, document.querySelector("main"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<main></main>
Instead of trying to create a class and call the function in another file, why not use React functional components and hooks?
Try something like this:
const Clock = () => {
const [hour, setHour] = useState();
const [min, setMin] = useState();
const [am, setAm] = useState(true);
useEffect(() => {
// Get your clock to work in here...
}, [hour, min, am]);
return (
<div>
{//This will post your clock here, and if you need the values, you
can set/use them individually as needed.}
{hour}:{min} {am ? 'am' : 'pm'}
{//The ternary statement will modify this portion for you in code.}
</div>
);
}
If you want to use the values globally, you may want to try using the React hook useContext(). This will allow you to access those specific values anywhere you want, but requires a bit more setup.
Context, if you don't know will turn your react app into Redux, without using Redux. Below is an example of what you need to do.
import { createContext } from "react";
export const QuizContext = createContext();
then you add the context to your App.js:
import { useState } from 'react';
import './App.css';
import MainMenu from './Components/MainMenu';
import Quiz from './Components/Quiz';
import EndScreen from './Components/EndScreen';
import { QuizContext } from './Helpers/Context';
function App() {
const [gameState, setGameState] = useState('Menu');
const [score, setScore] = useState(0);
return (
<div className="App">
<h1>Quiz App</h1>
<QuizContext.Provider value={{gameState, setGameState, score, setScore}}>
{gameState === 'Menu' && <MainMenu/>}
{gameState === 'Quiz' && <Quiz/>}
{gameState === 'EndScreen' && <EndScreen/>}
</QuizContext.Provider>
</div>
);
}
Then you can access the context from individual components as long as they are children of App.
Example:
import React, { useContext, useState } from 'react';
import { QuizContext } from '../Helpers/Context';
import {Questions} from '../Helpers/QuestionBank'
const Quiz = () => {
const [currentQuestion, setCurrentQuestion] = useState(0)
const [optionChosen, setOptionChosen] = useState('');
const {setGameState, score, setScore} = useContext(QuizContext);
const nextQuestion = () => {
Questions[currentQuestion].answer === optionChosen ? setScore(score + 1) : console.log(score);
setCurrentQuestion(currentQuestion + 1);
}
const finishQuiz = () => {
Questions[currentQuestion].answer === optionChosen ? setScore(score + 1) : console.log(score);
setGameState('EndScreen');
}
return (
<div className="Quiz">
<h1>{Questions[currentQuestion].prompt}</h1>
<div className="options">
<button onClick={() => setOptionChosen('optionA')}>{Questions[currentQuestion].optionA}</button>
<button onClick={() => setOptionChosen('optionB')}>{Questions[currentQuestion].optionB}</button>
<button onClick={() => setOptionChosen('optionC')}>{Questions[currentQuestion].optionC}</button>
<button onClick={() => setOptionChosen('optionD')}>{Questions[currentQuestion].optionD}</button>
</div>
{currentQuestion === Questions.length -1 ? <button onClick={finishQuiz}>Finish Quiz</button> : <button onClick={nextQuestion}>Next Question</button>}
</div>
)
}
export default Quiz
I learned this method from a Tutorial from PedroTech on YouTube. I followed along to create this. I wanted to make sure I didn't take credit for his work.
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;
})
},
[]
);
onclick of button need to update the state value from -12 to 50
any suggestion?
please refer below snippet
import React, { useState } from "react";
const Test = () => {
let commomRowData = {
2020: { firstLevel: { children: { secondLevel: { value: -12 } } } },
};
const [data, setData] = useState(commomRowData);
const updateData = () => {};
return (
<div>
<div>Testing</div>
<div>
<button onClick={updateData}> Click </button>
</div>
</div>
);
};
export default Test;
I would initialize useState's data with -12 and update only that value on click of the button with setData(50). This feels more straightforward than overwriting all of the object keys.
const Test = () => {
const [data, setData] = useState(-12);
let commomRowData = {
2020: { firstLevel: { children: { secondLevel: { value: data } } } }
};
const updateData = () => { setData(50)};
return (
<div>
<div>Testing</div>
<div>
<button onClick={updateData}> Click </button>
<pre>{JSON.stringify(commomRowData, null, 4)}</pre>
</div>
</div>
);
};
codeSandbox
I'm new to react and I'm trying component functional style.
I have simple todo list. I would like to strike out todo item from list using style property. From Chrome debug mode I do not see immediate reaction on checkbox changes, also Item is not striked out... It seams to me, that it is problem with how I manage state of components. I would appreciate some guidance.
App.js
import React, {useState} from 'react';
import Todos from "./components/Todos";
import './App.css'
const App = () => {
const [todos, setTodos] = useState(
[
{id: 1, title: 'Take out the trash', completed: false},
{id: 2, title: 'Dinner with wife', completed: false},
{id: 3, title: 'Meeting boss', completed: false}
]
);
const markComplete = id => {
console.log((new Date()).toString());
todos.map(todo => {
if (todo.id === id) {
todo.completed = ! todo.completed;
}
return todo;
});
setTodos(todos);
};
return (
<div className="App">
<Todos todos={todos} markComplete={markComplete}/>
</div>
);
};
export default App;
Todos.js
import React from "react";
import TodoItem from "./TodoItem";
const Todos = ({todos, markComplete}) => {
return (
todos.map(todo => (
<TodoItem key={todo.id} todoItem={todo} markComplete={markComplete} />
))
);
};
export default Todos;
TodoItem.js
import React from "react";
const TodoItem = ({todoItem, markComplete}) => {
const getStyle = () => {
console.log("style: " + todoItem.completed);
return {
background: '#f4f4f4',
padding: '10px',
borderBottom: '1px #ccc dotted',
textDecoration: todoItem.completed ? 'line-through' : 'none'
}
};
return (
<div style={getStyle()}>
<p>
<input type="checkbox" onChange={markComplete.bind(this, todoItem.id)}/>{' '}
{todoItem.title}
</p>
</div>
);
};
export default TodoItem;
I expect that this getStyle() will follow state... somehow...
Don't mutate state. In markComplete function, you are mutating the todos array directly. Change your function like this to avoid mutation
const markComplete = id => {
console.log((new Date()).toString());
let newTodos = todos.map(todo => {
let newTodo = { ...todo };
if (newTodo.id === id) {
newTodo.completed = !newTodo.completed;
}
return newTodo;
});
setTodos(newTodos);
};
Array.prototype.map() returns a new Array, which you are throwing away. You need to use the new array, e.g.:
const markComplete = id => {
...
setTodos(totos.map(...))