Make a React component rerender when data class property change - javascript

In my Typescript app, there's a class that represents some data. This class is being shared end to end (both front-and-back ends use it to structure the data). It has a property named items which is an array of numbers.
class Data {
constructor() {
this.items = [0];
}
addItem() {
this.items = [...this.items, this.items.length];
}
}
I'm trying to render those numbers in my component, but since modifying the class instance won't cause a re-render, I have to "force rerender" to make the new items values render:
const INSTANCE = new Data();
function ItemsDisplay() {
const forceUpdate = useUpdate(); // from react-use
useEffect(() => {
const interval = setInterval(() => {
INSTANCE.addItem();
forceUpdate(); // make it work
}, 2000);
return () => clearInterval(interval);
}, []);
return (
<div>
<h1>with class:</h1>
<div>{INSTANCE.items.map(item => <span>{item}</span>)}</div>
</div>
);
}
While this works, it has one major drawback: addItem() is not the only modification done to INSTANCE; This class has actually around 10 to 15 properties that represent different data parts. So, doing forceUpdate() wherever a modification happens is a nightmare. Not no mention, if this instance will be modified outside the component, I won't be able to forceUpdate() to sync the change with the component.
Using useState([]) to represent items will solve this issue, but as I said Data has a lot of properties, so as some functions. That's another nightmare.
I would like to know what's the best way of rendering data from a class instance, without rerender hacks or unpacking the whole instance into a local component state.
Thanks!
Here's a Codesandbox demo that shows the differences between using a class and a local state.

Here is an example of how you can make Data instance observable and use Effect in your components to observe changes in Data instance items:
const { useState, useEffect } = React;
class Data {
constructor() {
this.data = {
users: [],
products: [],
};
this.listeners = [];
}
addItem(type, newItem) {
this.data[type] = [...this.data[type], newItem];
//notify all listeners that something has been changed
this.notify();
}
addUser(user) {
this.addItem('users', user);
}
addProduct(product) {
this.addItem('products', product);
}
reset = () => {
this.data.users = [];
this.data.products = [];
this.notify();
};
notify() {
this.listeners.forEach((l) => l(this.data));
}
addListener = (fn) => {
this.listeners.push(fn);
//return the remove listener function
return () =>
(this.listeners = this.listeners.filter(
(l) => l !== fn
));
};
}
const instance = new Data();
let counter = 0;
setInterval(() => {
if (counter < 10) {
if (counter % 2) {
instance.addUser({ userName: counter });
} else {
instance.addProduct({ productId: counter });
}
counter++;
}
}, 500);
//custom hook to use instance
const useInstance = (instance, fn = (id) => id) => {
const [items, setItems] = useState(fn(instance.data));
useEffect(
() =>
instance.addListener((items) => setItems(fn(items))),
[instance, fn]
);
return items;
};
const getUsers = (data) => data.users;
const getProducts = (data) => data.products;
const Users = () => {
const users = useInstance(instance, getUsers);
return <pre>{JSON.stringify(users)}</pre>;
};
const Products = () => {
const products = useInstance(instance, getProducts);
return <pre>{JSON.stringify(products)}</pre>;
};
const App = () => {
const reset = () => {
instance.reset();
counter = 0;
};
return (
<div>
<button onClick={reset}>Reset</button>
<div>
users:
<Users />
</div>
<div>
products:
<Products />
</div>
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Related

React component rendered from object does not get unmounted

I have the following code, where I need to run clean-up when unmounting each component step. I've set a useEffect on each Step to check if the component has been unmounted. When the parent gets a new currentStep it swaps the currently active component but the clean-up never runs. I'm wondering if this has to do with the nature of the component being rendered from an object
const Step1 = () => {
useEffect(() => {
console.log("doing things here");
return () => {
console.log("clean-up should happen here but this won't print")
}
}, []}
}
const StepMap = {
step1: <Step1/>
step2: <Step2/>
step3: <Step3/>
}
const Parent = ({ currentStep }) => {
return (
<div>
{ StepMap[currentStep] }
</div>
)
}
Alternatively this piece of code does run the clean-up, but I do find the former cleaner
const Parent = ({ currentStep }) => {
return (
<div>
{ currentStep === "step1" && StepMap[currentStep]}
{ currentStep === "step2" && StepMap[currentStep]}
</div>
)
}
Why does the first approach not work? is there a way to make it work like the second while keeping a cleaner implementation?
if you want to write javascript inside jsx we have write it inside {} curly braces like this:
import React, { useEffect, useState } from "react";
const Step1 = () => {
useEffect(() => {
console.log("Step1 doing things here");
return () => {
console.log("Step1 clean-up should happen here but this won't print");
};
}, []);
return <div>stepOne</div>;
};
const Step2 = () => {
useEffect(() => {
console.log("Step2 doing things here");
return () => {
console.log("Step2 clean-up should happen here but this won't print");
};
}, []);
return <div>steptw0</div>;
};
const Step3 = () => {
useEffect(() => {
console.log("Step3 doing things here");
return () => {
console.log("Step3 clean-up should happen here but this won't print");
};
}, []);
return <div>stepthree</div>;
};
export const StepMap = {
step1: <Step1 />,
step2: <Step2 />,
step3: <Step3 />,
};
export const Parent = ({ currentStep }) => {
return <div>{StepMap[currentStep]}</div>;
};
const App = () => {
const [steps, setSteps] = React.useState("step1");
React.useEffect(() => {
setTimeout(() => setSteps("step2"), 5000);
setTimeout(() => setSteps("step3"), 15000);
}, []);
return <Parent currentStep={steps} />;
};
export default App;

React state doesn't refresh value

I'm now learning React and I have a problem with re-rendering component.
App.js code:
function App() {
const [expenses, setExpenses] = useState(INITIAL_EXPENSES);
const addNewExpenseHandler = (expense) => {
setExpenses((prevState) => {
return [expense, ...prevState];
}, changeYearHandler(filteredYear));
};
const filterExpenses = (expenses, year) => {
const newFilteredExpenses = expenses.filter((expense) => {
if (String(expense.date.getFullYear()) === year) {
return expense;
}
});
return newFilteredExpenses;
};
const [filteredYear, setFilteredYear] = useState('2019');
const [filteredExpenses, setFilteredExpenses] = useState(
filterExpenses(expenses, filteredYear)
);
const changeYearHandler = (value) => {
setFilteredYear(
value,
setFilteredExpenses(() => {
const newValue = filterExpenses(expenses, value);
return newValue;
})
);
};
return (
<>
<NewExpense onAddNewExpense={addNewExpenseHandler} />
<ExpenseFilter expenses={expenses} />
<ShowExpenses
onChangeYear={changeYearHandler}
data={filteredExpenses}
/>
</>
);
}
export default App;
The problem is that filteredExpenses isn't up-to-date. It's always retarded and it's the previous state. I was trying to call a changeYearHandler in callback of setExpenses and setFilteredExpense inside setFilteredYear but it's still doesn't work and I don't know why.
It feels like you're using a roundabout way to filter your expenses. What about just creating a memoized version of a filteredExpenses directly, using useMemo()?
const filteredExpenses = useMemo(() => {
return expenses.filter((expense) => {
if (String(expense.date.getFullYear()) === filteredYear) {
return expense;
}
});
}, [expenses, filteredYear]);
The dependency array will ensure that whenever expenses or filteredYear changes, then filteredExpenses will recompute and return a new filtered array (that is subsequently cached).

useEffect being triggered multiple times and I do not know what to change

Last edit of the night. Tried to clean some things up to make it easier to read. also to clarify what is going on around the useEffect. Because I am running react in strict mode everything gets rendered twice. The reference around the useEffect makes sure it only gets rendered 1 time.
Db is a firebase reference object. I am grabbing a list of league of legends games from my database.
one I have all my games in the snapshot variable, I loop through them to process each game.
each game contains a list of 10 players. using a puuId I can find a specific player. We then pull the data we care about in addChamp.
The data is then put into a local map. We continue to update our local map untill we are done looping through our database data.
After this I attempt to change our state variable in the fetchMatches function.
My issue now is that I am stuck in an infinite loop. I think this is because I am triggering another render after the state gets changed.
import { useState, useEffect, /*useCallback,*/ useRef } from 'react'
import Db from '../Firebase'
const TotGenStats = ({ player }) => {
const [champs, setChamps] = useState(new Map())
var init = new Map()
var total = 0
console.log("entered stats")
const addChamp = /*useCallback(*/ (item) => {
console.log("enter add champ")
var min = item.timePlayed/60
//var sec = item.timePlayed%60
var kda = (item.kills + item.assists)/item.deaths
var dub = 0
if(item.win){
dub = 1
}
var temp = {
name: item.championName,
avgCs: item.totalMinionsKilled,
csMin: item.totalMinionsKilled/min,
kds: kda,
kills: item.kills,
deaths: item.deaths,
assists: item.assists,
wins: dub,
totalG: 1
}
init.set(item.championName, temp)
//setChamps(new Map(champs.set(item.championName, temp)))
}//,[champs])
const pack = /*useCallback( /*async*/ (data) => {
console.log("enter pack")
for(const item of data.participants){
//console.log(champ.assists)
if(item.puuid === player.puuid){
console.log(item.summonerName)
if(init.has(item.championName)){//only checking init??
console.log("update champ")
}
else{
console.log("add champ")
/*await*/ addChamp(item)
}
}
}
}/*,[addChamp, champs, player.puuid])*/
const fetchMatches = async () => {
console.log("enter fetch matches")
Db.collection("summoner").doc(player.name).collection("matches").where("queueId", "==", 420)
.get()
.then((querySnapshot) => {
querySnapshot.forEach(async (doc) => {
//console.log("loop")
console.log(doc.id, " => ", doc.data());
console.log("total: ", ++total);
await pack(doc.data());
});
})
.then( () => {
setChamps(init)
})
.catch((error) => {
console.log("error getting doc", error);
});
}
const render1 = useRef(true)
useEffect( () => {
console.log("enter use effect")
if(render1.current){
render1.current = false
}
else{
fetchMatches();
}
})
return(
<div>
<ul>
{[...champs.keys()].map( k => (
<li key={k}>{champs.get(k).name}</li>
))}
</ul>
</div>
)
}
export default TotGenStats
Newest Version. no longer infinitly loops, but values do not display/render.
import { useState, useEffect } from 'react'
import Db from '../Firebase'
const TotGenStats = ({ player }) => {
const [champs, setChamps] = useState(new Map())
var total = 0
console.log("entered stats")
const addChamp = /*useCallback(*/ (item) => {
console.log("enter add champ")
var min = item.timePlayed/60
//var sec = item.timePlayed%60
var kda = (item.kills + item.assists)/item.deaths
var dub = 0
if(item.win){
dub = 1
}
var temp = {
name: item.championName,
avgCs: item.totalMinionsKilled,
csMin: item.totalMinionsKilled/min,
kds: kda,
kills: item.kills,
deaths: item.deaths,
assists: item.assists,
wins: dub,
totalG: 1
}
return temp
}
useEffect(() => {
var tempChamp = new Map()
Db.collection("summoner").doc(player.name).collection("matches").where("queueId","==",420)
.get()
.then((querySnapshot) => {
querySnapshot.forEach(async (doc) => {
console.log(doc.id," => ", doc.data());
console.log("total: ", ++total);
for(const person of doc.data().participants){
if(player.puuid === person.puuid){
console.log(person.summonerName);
if(tempChamp.has(person.championName)){
console.log("update ", person.championName);
//add update
}else{
console.log("add ", person.championName);
var data = await addChamp(person);
tempChamp.set(person.championName, data);
}
}
}
})//for each
setChamps(tempChamp)
})
},[player.name, total, player.puuid]);
return(
<div>
<ul>
{[...champs.keys()].map( k => (
<li key={k}>{champs.get(k).name}</li>
))}
</ul>
</div>
)
}
export default TotGenStats
useEffect will be called only once when you will not pass any argument to it and useEffect works as constructor hence its not possible to be called multiple times
useEffect( () => {
},[])
If you pass anything as argument it will be called whenever that argument change is triggered and only in that case useEffect will be called multiple times.
useEffect( () => {
},[arg])
Though whenever you update any state value in that case component will re-render. In order to handle that situation you can use useCallback or useMemo.
Also for map operation directly doing it on state variable is not good idea instead something like following[source]:
const [state, setState] = React.useState(new Map())
const add = (key, value) => {
setState(prev => new Map([...prev, [key, value]]))
}
I have made some edits to your latest code try following:
import { useState, useEffect, useRef } from "react";
import Db from "../Firebase";
const TotGenStats = ({ player }) => {
const [champs, setChamps] = useState(new Map());
const addChamp = (item) => {
let min = item.timePlayed / 60;
let kda = (item.kills + item.assists) / item.deaths;
let dub = null;
if (item.win) {
dub = 1;
} else {
dub = 0;
}
let temp = {
name: item.championName,
avgCs: item.totalMinionsKilled,
csMin: item.totalMinionsKilled / min,
kds: kda,
kills: item.kills,
deaths: item.deaths,
assists: item.assists,
wins: dub,
totalG: 1,
};
setChamps((prev) => new Map([...prev, [item.championName, temp]]));
};
const pack = (data) => {
for (const item of data.participants) {
if (item.puuid === player.puuid) {
if (!champs.has(item.championName)) {
addChamp(item);
}
}
}
};
const fetchMatches = async () => {
Db.collection("summoner")
.doc(player.name)
.collection("matches")
.where("queueId", "==", 420)
.get()
.then((querySnapshot) => {
querySnapshot.forEach(async (doc) => {
await pack(doc.data());
});
})
.catch((error) => {});
};
const render1 = useRef(true);
useEffect(() => {
fetchMatches();
});
return (
<div>
<ul>
{[...champs.keys()].map((k) => (
<li key={k}>{champs.get(k).name}</li>
))}
</ul>
</div>
);
};
export default TotGenStats;

Wait for DOM element then react - Reactjs

Hi I have a class component as shown below:
class SomeComponent extends Component {
componentDidMount = () => {
const divElement = document.getElementbyId('id'); // this element could take a few seconds to load
if (props.something1 && props.something2) {
..do something with divElement's width
}
}
render() {
return ....
}
}
I want to wait until divElement is loaded, or trigger an event when divElement is loaded so I can do my calculation later, tried adding setTimeout which did not work
Two answers for you:
Use a ref (if your component renders the element)
If the element is rendered by your component, use a ref.
Use a MutationObserver (if the element is outside React)
If the element is completely outside the React part of your page, I'd look for it with getElementById as you are, and if you don't find it, use a MutationObserver to wait for it to be added. Don't forget to remove the mutation observer in componentWillUnmount.
That would look something like this:
componentDidMount = () => {
const divElement = document.getElementbyId('id');
if (divElement) {
this.doStuffWith(divElement);
} else {
this.observer = new MutationObserver(() => {
const divElement = document.getElementbyId('id');
if (divElement) {
this.removeObserver();
this.doStuffWith(divElement);
}
});
this.observer.observe(document, {subtree: true, childList: true});
}
}
componentWillUnmount = () => {
this.removeObserver();
}
removeObserver = () => {
if (this.observer) {
this.observer.disconnect();
this.observer = null;
}
}
(You may have to tweak that, it's off-the-cuff; see the MutationObserver documentation for details.)
This is a dumb solution but it gets its jobs done:
const getElementByIdAsync = id => new Promise(resolve => {
const getElement = () => {
const element = document.getElementById(id);
if(element) {
resolve(element);
} else {
requestAnimationFrame(getElement);
}
};
getElement();
});
To use it:
componentDidMount = async () => {
const divElement = await getElementByIdAsync('id');
if (props.something1 && props.something2) {
// ..do something with divElement's width
}
}
You need to use the componentDidUpdate hook instead of componentDidMount hook. And better to use ref rather than getting div element by it's id:
componentDidUpdate() {
if (props.something1 && props.something2) {
// use divElementRef to interact with
}
}
The answer of #hao-wu is great. Just if anyone wonders how to use it with hooks here is my snippet.
const Editor = () => {
const [editor, setEditor] = useState<SimpleMDE | null>(null);
useEffect(() => {
(async () => {
const initialOptions = {
element: await getElementByIdAsync(id),
initialValue: currentValueRef.current
};
setEditor(
new SimpleMDE({
element: await getElementByIdAsync(id),
initialValue: currentValueRef.current
})
);
})();
}, [id]);
// Other effects that are looking for `editor` instance
return <textarea id={id} />;
};
Otherwise, the constructor of SimpleMDE cannot find an element and everything is broken :)
I guess you can adjust it to your use-case quite easily.
Most of the time useRef just works, but not in this scenario.
Here's my hook version to #hao-wu's answer
const useGetElementAsync = (query) => {
const [element, setElement] = useState(null);
useEffect(() => {
(async () => {
let element = await new Promise((resolve) => {
function getElement() {
const element = document.querySelector(query);
if (element) {
resolve(element);
} else {
console.count();
// Set timeout isn't a must but it
// decreases number of recursions
setTimeout(() => {
requestAnimationFrame(getElement);
}, 100);
}
};
getElement();
});
setElement(element);
})();
}, [query]);
return element;
};
To use it:
export default function App() {
const [isHidden, setIsHidden] = useState(true);
const element = useGetElementAsync(".myElement");
// This is to simulate element loading at a later time
useEffect(() => {
setTimeout(() => {
setIsHidden(false);
}, 1000);
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
{!isHidden && (
<h2 className="myElement">My tag name is {element?.tagName}</h2>
)}
</div>
);
}
Here's the codesandbox example
You can do something like;
componentDidMount() {
// Triggering load of some element
document.querySelector("#id").onload = function() {
// Write your code logic here
// code here ..
}
}
Reference https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onload

how can get data from api to child combobox in react

i can't move my code to child component so how can i solve this problem. so that i can use my api data to my combobox
async getData() {
const PROXY_URL = 'https://cors-anywhere.herokuapp.com/';
const URL = 'my-api';
const res = await axios({
method: 'post', // i get data from post response
url: PROXY_URL+URL,
data: {
id_user : this.props.user.id_user
}
})
const {data} = await res;
this.setState({ user : data.data})
}
componentDidMount = () => {
this.getData()
}
and i send my state to my combobox in child component
<ComboBox
name="pic"
label="Default PIC"
placeholder=""
refs={register({ required: true })}
error={errors.PIC}
message=""
labelFontWeight="400"
datas={this.state.user}
></ComboBox>
combobox code :
right now I just want to be able to console my index data
let ComboBox = props => {
useEffect(() => {
for (let i = 0; i < props.datas.length; i++) {
console.log(i) //this can use if using hard props or manual data
props.datas[i].selected = false;
props.datas[i].show = true;
}
setDatas(props.datas);
document.addEventListener('click', e => {
try {
if (!refDivComboBox.current.contains(e.target)) {
setIsOpen(false);
}
} catch (error) {}
});
unSelectedComboBox();
}, []);
export default ComboBox;
I think you are missing the props.datas dependency in your ComboBox component.
let ComboBox = props => {
useEffect(() => {
for (let i = 0; i < props.datas.length; i++) {
console.log(i) //this can use if using hard props or manual data
props.datas[i].selected = false;
props.datas[i].show = true;
}
setDatas(props.datas);
document.addEventListener('click', e => {
try {
if (!refDivComboBox.current.contains(e.target)) {
setIsOpen(false);
}
} catch (error) {}
});
unSelectedComboBox();
}, [props.datas]); // THIS IS THE DEPENDENCY ARRAY, try adding props.datas here
export default ComboBox;
Here is a brief explanation of useEffect.
Used as componentDidMount():
useEffect(() => {}, [])
Used as componentDidUpdate() (triggers after props.something changes):
useEffect(() => {}, [props.something])
Used as componenWillUnmount():
useEffect(() => {
return () => { //Unmount }
}, [])
This, of course, is a really simple explanation, and this can be used much better when properly learned. Take a look at some tutorials utilizing useState, try to find in particular migrations from this.state to useState - those might help you wrap your head around useState

Categories