React Memo still rerenders on state update - javascript

I am wondering why the console.log({ routes }) is outputting on a search useState update without the search having filtered the data. I have memoized and useCallbacked things.
import { memo, useCallback, useMemo, useState } from 'react'
import * as stations from './stations.json'
import './App.css';
const StyledContainer = {
"cursor": "pointer",
"display": "flex",
"flex-direction": "row"
}
const StyledSegment = {
"justify-content": "space-between",
"text-align": "left",
"width": "33.3%"
}
const baseUrl = 'https://api.wheresthefuckingtrain.com/by-id'
const StationComponent = (fetchSchedule, s) => {
const { id, location, name, routes = false, stops } = s
console.log({ routes }) // THIS RERENDERS
return (
<div key={id} onClick={() => { fetchSchedule(id) }}>
<h3>{name}</h3>
<h4>{location}</h4>
{routes && routes.N.length > 0 && <div><label>North</label><ul>{routes.N.map(({ route, time }, idx) => <li key={idx}>Route: {route}<br></br>{new Date(time).toLocaleTimeString()}</li>)}</ul></div>}
{routes && routes.N.length > 0 && <div><label>South</label><ul>{routes.S.map(({ route, time }, idx) => <li key={idx}>Route: {route}<br></br>{new Date(time).toLocaleTimeString()}</li>)}</ul></div>}
</div>
)
}
const MemoizedStationComponent = memo(StationComponent)
function App() {
const [filteredStationData, setFilteredStationData] = useState(stations)
const [search, setSearch] = useState() // station name
const [stationData, setStationData] = useState(stations)
console.log({stations})
const fetchSchedule = useCallback(async (id) => {
const res = await fetch(`${baseUrl}/${id}`, { method: 'get' })
const json = await res.json()
console.log("apiCall")
const copyStationData = JSON.parse(JSON.stringify(stations))
copyStationData[id].routes = json.data[0]
setStationData(copyStationData)
})
const filterStations = useCallback(() => {
const filteredStations = Object.values(stationData).filter((s) => s.name.includes(search))
setFilteredStationData(filteredStations)
})
return (
<div className="App" style={StyledContainer}>
<header className="App-header">
<input placeholder="Name" onChange={(e) => { setSearch(e.target.value) }} /><button onClick={() => { filterStations() }}>Search</button>
<div style={StyledSegment}>
{Object.values(filteredStationData).map(s => <MemoizedStationComponent fetchSchedule={fetchSchedule} s={s} />)}
</div>
</header>
</div>
);
}
export default App;

Few points:
You haven't provided dependencies to useCallback.
Pass an inline callback and an array of dependencies. useCallback will
return a memoized version of the callback that only changes if one of
the dependencies has changed. This is useful when passing callbacks to
optimized child components that rely on reference equality to prevent
unnecessary renders (e.g. shouldComponentUpdate).
Also I notice you haven't passed keys here: <MemoizedStationComponent fetchSchedule={fetchSchedule} s={s} />
; this can become problematic (you should have received warning), and you can end up in corrupt state etc., if the elements of the array which you are mapping can reorder for example.
And general note, if you pass an object as prop to memoized component, make sure it is not recreated on each render, or it will break memoization.

Just add dependencies for useCallback to prevent unnecessary re-renders of your station component.
const fetchSchedule = useCallback(async (id) => {
const res = await fetch(`${baseUrl}/${id}`, { method: 'get' })
const json = await res.json()
console.log("apiCall")
const copyStationData = JSON.parse(JSON.stringify(stations))
copyStationData[id].routes = json.data[0]
setStationData(copyStationData)
}, [stations])
const filterStations = useCallback(() => {
const filteredStations = Object.values(stationData).filter((s) => s.name.includes(search))
setFilteredStationData(filteredStations)
}, [search])
Now each time your stations or search value is changed. Your component will re-render.

Related

why the changing state of my component do not transmit when i export this component?

I have a hook that rules my requests. It has state "loading" that becoming true while loading
export const useHttp = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const request = useCallback(async (url, method = 'GET', body = null, headers = {'Content-Type': 'application/json' }) => {
setLoading(true);
try {
const res = await fetch(url, {method, body, headers});
if (!res.ok) {
throw new Error(`Could not fetch ${url}, status: ${res.status}`);
}
const data = await res.json();
setLoading(false);
return data;
} catch(e) {
setLoading(false);
setError(e.message);
throw e;
}
}, [])
const clearError = useCallback(() => {
setError(null);
}, [])
return {
loading,
error,
request,
clearError
}
}
Also i have a service that makes requests:
import { useHttp } from "../hooks/http.hook";
const useNasaService = () => {
const { loading, error, request, clearError } = useHttp();
const _apiBase = 'https://api.nasa.gov/';
const _apiKey = 'api_key=DEMO_KEY';
const getMissionManifest = async (rover) => {
const res = await request(`${_apiBase}mars-photos/api/v1/manifests/${rover}/?${_apiKey}`);
return _transformManifestData(res.photo_manifest);
}
const getImagesData = async (rover, sol, page = 1) => {
const res = await request(`${_apiBase}mars-photos/api/v1/rovers/${rover}/photos?sol=${sol}&page=${page}&${_apiKey}`);
return res.photos.map(_transformImagesData);
}
const _transformImagesData = (data) => {
return {
id: data.id,
sol: data.sol,
earthDate: data.earth_date,
path: data.img_src,
camera: data.camera.full_name,
rover: data.rover.name
}
}
const _transformManifestData = (data) => {
return {
landingDate: data.landing_date,
launchDate: data.launch_date,
maxDate: data.max_date,
maxSol: data.max_sol,
name: data.name,
photos: data.photos,
status: data.status,
totalPhotos: data.total_photos
}
}
return {
loading,
error,
clearError,
getImagesData,
getMissionManifest
}
}
export default useNasaService;
Finally i have a component that needs state "loading" for disabling the inputs.
The question is why "loading" is never getting true in this component:
import useNasaService from '../../services/useNasaService';
const RoverFilter = (props) => {
const { loading } = useNasaService();
console.log(loading); /* always false */
const onRadioChange = (e) => {
props.onRoverSelected(e.target.value);
props.onRoverClicked(e.target.value);
}
return (
<div className="roverFilter" >
<h2 className="roverFilter__title">Select rover</h2>
<div className="roverFilter__inputs">
<label htmlFor="curiosity">Curiosity</label>
<input disabled={loading} type="radio" name="rover-choise" id="curiosity" value="curiosity" onChange={onRadioChange}/>
<label htmlFor="opportunity">Opportunity</label>
<input disabled={loading} type="radio" name="rover-choise" id="opportunity" value="opportunity" onChange={onRadioChange}/>
<label htmlFor="spirit">Spirit</label>
<input disabled={loading} type="radio" name="rover-choise" id="spirit" value="spirit" onChange={onRadioChange}/>
<label htmlFor="perseverance">Perseverance</label>
<input disabled={loading} type="radio" name="rover-choise" id="perseverance" value="perseverance" onChange={onRadioChange}/>
</div>
</div>
)
}
export default RoverFilter;
By the way, in my app there are another components, where "loading" becoming true without any problems. I cant see the difference.
for example, here loading works good:
import { useEffect, useState } from 'react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import useNasaService from '../../services/useNasaService';
import ImageGallerySkeleton from '../imageGallerySkeleton/ImageGallerySkeleton';
import Spinner from '../spinner/Spinner';
import ErrorMessage from '../errorMessage/ErrorMessage';
import SliderModal from '../sliderModal/SliderModal';
const ImageGallery = (props) => {
const {loading, getImagesData, clearError, error} = useNasaService();
const [imagesData, setImagesData] = useState([]);
const [nextPage, setNextPage] = useState(1);
const [firstLoading, setFirstLoading] = useState(true);
const [imagesDataLoaded, setImagesDataLoaded] = useState(false);
const [itemIndex, setItemIndex] = useState(0);
const [sliderOpen, setSliderOpen] = useState(false);
const transitionDuration = 1000;
const onImagesDataLoaded = (newData) => {
setImagesData(data => [...data, ...newData]);
setNextPage(page => page + 1);
setFirstLoading(false);
setImagesDataLoaded(true);
}
const onRequestImages = (rover, sol, page) => {
clearError();
if (!rover || !sol) return;
getImagesData(rover, sol, page)
.then(onImagesDataLoaded);
}
const onSliderClosed = () => {
setSliderOpen(false);
}
useEffect(() => {
onRequestImages(props.selectedRover, props.selectedSol, nextPage);
// eslint-disable-next-line
}, [props.selectedRover, props.selectedSol])
if (sliderOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "visible";
}
function renderItemList(arr) {
const itemList = arr.map((item, i) => {
return (
<CSSTransition
key={item.id}
in={imagesDataLoaded}
timeout={transitionDuration}
classNames='imageGallery__card'>
<li className="imageGallery__card"
onClick={() => {
setSliderOpen(true);
setItemIndex(i);
}}>
<img src={item.path} alt="img from mars"/>
<div className="imageGallery__descr">
<ul>
<li>Rover: {item.rover}</li>
<li>Earth_date: {item.earthDate}</li>
<li>Sol: {item.sol}</li>
<li>{item.camera}</li>
</ul>
</div>
</li>
</CSSTransition>
)
})
return (
<ul className="imageGallery__list">
<TransitionGroup component={null}>
{itemList}
</TransitionGroup>
</ul>
)
}
const spinner = loading && firstLoading ? <Spinner/> : null;
const skeleton = imagesData.length === 0 && firstLoading && !loading && !error ? <ImageGallerySkeleton/> : null;
const items = renderItemList(imagesData);
const errorMessage = error ? <ErrorMessage/> : null;
const counter = imagesData.length === 0 || error ? null :
<h2 className="imageGallery__title">
Showed {loading ? "..." : imagesData.length} photos of {props.totalPhotosInSol}
</h2>
const button = props.totalPhotosInSol === imagesData.length ? null :
<button
onClick={() => onRequestImages(props.selectedRover, props.selectedSol, nextPage)}
disabled={loading}
className="imageGallery__btn">{loading ? "Loading..." : "Load next page" }
</button>
const slider = <SliderModal
open={sliderOpen}
items={imagesData}
slideIndex={itemIndex}
onSliderClosed={onSliderClosed} />
const wrapStyles = firstLoading && loading ? {"padding": "50px"} : null;
return (
<section className="imageGallery" style={wrapStyles}>
{counter}
{spinner}
{skeleton}
{imagesData.length === 0 && !firstLoading ?
<h2 className="imageGallery__title">There is no photo for this sol</h2> :
items
}
{button}
{errorMessage}
{slider}
</section>
)
}
export default ImageGallery;
When you call useState in two different components, those states are independant from eachother. This is still true if you move the useState calls inside a custom hook. If two components call useNasaService (which calls useHttp, which calls useState), then the two components are creating their own states and own functions. If component A starts loading data, that will have no effect on component B.
So ImageGallery is working because it makes a call to getImagesData. This sets the loading state of ImageGallery to true. No other components are affected by this though. When the loading finishes, ImageGallery will set state to have the new data, but again, no other components can use this. RoverFilter on the other hand never calls getImagesData, so its loading state stays false, and it never gets any data.
In react, the typical way to share data is to lift state up. You have a component higher up in the tree, which is responsible for loading the data and setting state. That component then passes the data and functions down to any children that need it. You can either pass the data down using props, or if you need to pass the data a long distance you can consider using context instead.
There's also a wide variety of 3rd party libraries which can be used to manage global state. For example, Redux, Jotai, Zustand, MobX. These can make it simpler to share data between components in far-flung parts of the component tree.

How to get around React setState Delay?

I have been stuck on the simple issue of the common React setState delay. I am currently looking to update an object within an array, by saving it to a state variable "newStud" within a child component, and pass it into a parent component to be utilized for a filtering function. My current issue is that state only updates completely after the second submission of an entry on my site. Thus, when the filter function in the parent component aims to read the array being passed in, it throws errors as the initial declaration of state is what is passed in. My question is if there is some way I can adjust for that delay in updating that information without having to break apart my larger components into smaller more manageable components?
For reference, here is the code I am utilizing for the child component (the issue is present in my "addTag" function):
import React, {useState, useEffect} from 'react';
import './studentcard.css';
import { Tags } from '../Tags/tags.js';
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import { faPlus } from '#fortawesome/free-solid-svg-icons';
import { faMinus } from '#fortawesome/free-solid-svg-icons';
export function StudentCard({student, upStuds}) {
const [newStud, setNewStud] = useState({});
const [clicked, setClicked] = useState(false);
const [tag, setTag] = useState('');
// switches boolean to opposite value for plus/minus icon display
const onClick = () => {
setClicked(!clicked);
};
// triggers the addTag function to push a tag to the array within the student object
const onSubmit = async (e) => {
e.preventDefault();
await addTag(tag);
};
// captures the values being entered into the input
const onChange = (e) => {
setTag(e.target.value);
};
// this pushes the tag state value into the array that is located in the student object being passed down from the parent component
// it is meant to save the new copy of the "student" value in "newStuds" state variable, and pass that into the callback func
// ********** here is where I am experiencing my delay ************
const addTag = () => {
student.tags.push(tag);
setNewStud({...student});
upStuds(newStud);
setTag('');
};
let scores;
if (clicked !== false) {
scores = <ul className='grades-list'>
{student.grades.map((grade, index) => <li key={index} className='grade'>Test {(index + 1) + ':'} {grade}%</li>)}
</ul>;
}
return (
<div className='studentCard' >
<div className='pic-and-text'>
<img className='student-image' alt='' src={student.pic}></img>
<section className='right-side'>
<h3 id='names'>{student.firstName.toUpperCase() + ' ' + student.lastName.toUpperCase()}</h3>
<h4 className='indent'>Email: {student.email}</h4>
<h4 className='indent'>Company: {student.company}</h4>
<h4 className='indent'>Skill: {student.skill}</h4>
<h4 className='indent'>Average: {student.grades.reduce((a, b) => parseInt(a) + parseInt(b), 0) / student.grades.length}%</h4>
{scores}
<Tags student={student}/>
<form className='tag-form' onSubmit={onSubmit}>
<input className='tag-input' type='text' placeholder='Add a tag' onChange={onChange} value={tag}></input>
</form>
</section>
</div>
<FontAwesomeIcon icon={clicked !== false ? faMinus : faPlus} className='icon' onClick={onClick}/>
</div>
)
};
And if necessary, here is the Parent Component which is attempting to receive the updated information (the callback function I am using to fetch the information from the child component is called "upStuds") :
import React, {useState, useEffect} from 'react';
import './dashboard.css';
import {StudentCard} from '../StudentCard/studentcard';
import axios from 'axios';
export function Dashboard() {
const [students, setStudents] = useState([]);
const [search, setSearch] = useState('');
const [tagSearch, setTagSearch] = useState('');
useEffect(() => {
const options = {
method: 'GET',
url: 'https://api.hatchways.io/assessment/students'
};
var index = 0;
function genID() {
const result = index;
index += 1;
return result;
};
axios.request(options).then((res) => {
const students = res.data.students;
const newData = students.map((data) => {
const temp = data;
temp["tags"] = [];
temp["id"] = genID();
return temp;
});
setStudents(newData);
}).catch((err) => {
console.log(err);
});
}, []);
const onSearchChange = (e) => {
setSearch(e.target.value);
};
const onTagChange = (e) => {
setTagSearch(e.target.value);
};
// here is the callback function that is not receiving the necessary information on time
const upStuds = (update) => {
let updatedCopy = students;
updatedCopy.splice(update.id, 1, update);
setStudents(updatedCopy);
};
// const filteredTagged = tagList.filter
return (
<div className='dashboard'>
<input className='form-text1' type='text' placeholder='Search by name' onChange={onSearchChange}></input>
<input className='form-text2' type='text' placeholder='Search by tag' onChange={onTagChange}></input>
{students.filter((entry) => {
const fullName = entry.firstName + entry.lastName;
const fullNameWSpace = entry.firstName + ' ' + entry.lastName;
if (search === '') {
return entry;
} else if (entry.firstName.toLowerCase().includes(search.toLowerCase()) || entry.lastName.toLowerCase().includes(search.toLowerCase())
|| fullName.toLowerCase().includes(search.toLowerCase()) || fullNameWSpace.toLowerCase().includes(search.toLowerCase())) {
return entry;
}
}).map((entry, index) => {
return (<StudentCard student={entry} key={index} upStuds={upStuds} />)
})}
</div>
)
};
Please let me know if I need to clarify anything! Thanks for any assistance!
setNewStud({...student});
upStuds(newStud);
If you want to send the new state to upStuds, you can assign it to a variable and use it twice:
const newState = {...student};
setNewStud(newState);
upStuds(newState);
Additionally, you will need to change your upStuds function. It is currently mutating the existing students array, and so no render will occur when you setStudents. You need to copy the array and edit the copy.
const upStuds = (update) => {
let updatedCopy = [...students]; // <--- using spread operator to create a shallow copy
updatedCopy.splice(update.id, 1, update);
setStudents(updatedCopy);
}

react constantly calling the API when using .map to go through the list

i have setup a strapi API, and i am using react to consume that API (with Axios).
here's what the code look like inside App.js
import axios from "axios";
import React, {useEffect, useState} from "react";
import LineCard from "./components/Linecard"
function App() {
// don't mind the URL i will fix them later
const root = "http://localhost:1337"
const URL = 'http://localhost:1337/pick-up-lines'
// this is the "messed up" data from strapi
const [APIdata, setAPIdata] = useState([])
//this is the clean data
const [lines, setLines] = useState([])
// the array that i will be using later to "setLines" state
const linesFromApi = APIdata.map((line, index) => {
const profileImage = root + line.users_permissions_user.profilePicture.formats.thumbnail.url
const userName = line.users_permissions_user.username
const title = line.title
const lineBody = line.line
const rating = line.rating
const categories = line.categories.map((category, index) => category.categoryName)
return {
profileImage,
userName,
title,
lineBody,
rating,
categories
}
})
useEffect(() => {
// calling the API with get method to fetch the data and store it inside APIdata state
axios.get(URL).then((res) => {
setAPIdata(res.data)
})
setLines(linesFromApi)
}, [URL, linesFromApi])
return (
<div>
// mapping through the lines list and rendering a card for each element
{lines.map((line, index) => <LineCard line={line} />)}
</div >
);
}
export default App;
i know for sure that this is causing the problem
return (
<div>
{lines.map((line, index) => <LineCard line={line} />)}
</div >
);
my problem is that react keeps sending GET requests constantly, and i want it to stop after the first time it has the list.
how can i do that!
Try adding a check in your hook so that it restricts the api call if the value is already set.
Something like this
useEffect(() => {
if(lines.length === 0){
axios.get(URL).then((res) => {
setAPIdata(res.data)
})
setLines(linesFromApi)
}
}, [URL, linesFromApi])
You need to add the key property to the element in a map.
<div>
{lines.map((line, index) => <LineCard key={index} line={line} />)}
</div>

Get value from response and transfer to another component in React

I have this handleSubmit that returns me a key (verifyCode) that I should use in another component. How can I pass this verifyCode to another component?
const SendForm = ({ someValues }) => {
const handleSubmitAccount = () => {
dispatch(createAccount(id, username))
.then((response) => {
// I get this value from data.response, its works
const { verifyCode } = response;
})
.catch(() => {
});
};
return(
//the form with handleSubmitAccount()
)
}
export default SendForm;
The other component is not a child component, it is loaded after this submit step. But I don't know how to transfer the const verifyCode.
This is the view where the components are loaded, it's a step view, one is loaded after the other, I need to get the const verifyCode in FormConfirmation
<SendForm onSubmit={handleStepSubmit} onFieldSubmit={handleFieldSubmit} />
<FormConfirmation onSubmit={handleStepSubmit} onFieldSubmit={handleFieldSubmit} />
Does anyone know how I can do this?
You need to move up the state to a component that has both as children and then pass down a function that updates as a prop
import React from "react";
export default function App() {
const [value, setValue] = React.useState(0);
return (
<div className="App">
<Updater onClick={() => setValue(value + 1)} />
<ValueDisplay number={value} />
</div>
);
}
const Updater = (props) => <div onClick={props.onClick}>Update State</div>;
const ValueDisplay = (props) => <div>{props.number}</div>;
Check out the docs here
For more complex component structures or where your passing down many levels you may want to look into reactContext
import React from "react";
//Set Default Context
const valueContext = React.createContext({ value: 0, setValue: undefined });
export default function App() {
const [value, setValue] = React.useState(0);
return (
<div className="App">
{/** Pass in state and setter as value */}
<valueContext.Provider value={{ value: value, setValue }}>
<Updater />
<ValueDisplay />
</valueContext.Provider>
</div>
);
}
const Updater = () => {
/** Access context with hook */
const context = React.useContext(valueContext);
return (
<div onClick={() => context.setValue(context.value + 1)}>Update State</div>
);
};
const ValueDisplay = () => {
/** Access context with hook */
const context = React.useContext(valueContext);
return <div>{context?.value}</div>;
};

How to remove an element from an array dynamically? React.js

There are two components, I want to implement an element array using the useContext hook, but when the button is clicked, the element is not removed, but on the contrary, there are more of them. Tell me what is wrong here. I would be very grateful!
First component:
import React from 'react';
import CartItem from './CartItem';
import Context from '../Context';
function Cart() {
let sum = 0;
let arrPrice = [];
let [products, setProducts] = React.useState([]);
let loacalProsucts = JSON.parse(localStorage.getItem('products'));
if(loacalProsucts === null) {
return(
<div className="EmptyCart">
<h1>Cart is empty</h1>
</div>
)
} else {
{loacalProsucts.map(item => products.push(item))}
{loacalProsucts.map(item => arrPrice.push(JSON.parse(item.total)))}
}
for(let i in arrPrice) {
sum += arrPrice[i];
}
function removeItem(id) {
setProducts(
products.filter(item => item.id !== id)
)
}
return(
<Context.Provider value={{removeItem}}>
<div className="Cart">
<h1>Your purchases:</h1>
<CartItem products = {products} />
<h1>Total: {sum}$</h1>
</div>
</Context.Provider>
)
}
Second component:
import React, { useContext } from 'react';
import Context from '../Context';
function CartList({products}) {
const {removeItem} = useContext(Context);
return(
<div className="CartList">
<img src={products.image} />
<h2>{products.name}</h2>
<h3 className="CartInfo">{products.kg}kg.</h3>
<h2 className="CartInfo">{products.total}$</h2>
<button className="CartInfo" onClick={() => removeItem(products.id)}>×</button>
</div>
);
}
export default CartList;
Component with a context:
import React from 'react';
const Context = React.createContext();
export default Context;
Adding to the comment above ^^
It's almost always a mistake to have initialization expressions inside your render loop (ie, outside of hooks). You'll also want to avoid mutating your local state, that's why useState returns a setter.
Totally untested:
function Cart() {
let [sum, setSum] = React.useState();
const loacalProsucts = useMemo(() => JSON.parse(localStorage.getItem('products')));
// Init products with local products if they exist
let [products, setProducts] = React.useState(loacalProsucts || []);
useEffect(() => {
// This is actually derived state so the whole thing
// could be replaced with
// const sum = products.reduce((a, c) => a + c?.total, 0);
setSum(products.reduce((a, c) => a + c?.total, 0));
}, [products]);
function removeItem(id) {
setProducts(
products.filter(item => item.id !== id)
)
}
...

Categories

Resources