I have an large application with nested components. Each component can be individually modified but I would like to be able to have the data persist between sessions. Currently, I am having each component be in charge of saving updates using the browsers LocalStorage but I would ultimately like to be able to export all of the app data to a single JSON file and use that to save the user's progress in the app.
My best idea, as of posting, is to use a prop to trigger a callback function passed to all the children. When a prop (which I called msgPort is changed) each child component will pass their data up to the parent component. So far it works as I would expect but it feels like this method is bad practice and not "React-ful". Is this method acceptable, or are there some pitfalls of scaling this method up to a much larger application? Any advice/feedback is much appreciated.
Here is a working example:
https://codesandbox.io/s/save-nested-data-s5umb?file=/src/App.js
And here is the same code from the CodeSandbox
import "./styles.css";
import { useEffect, useState } from "react";
import "bootstrap/dist/css/bootstrap.css";
function A(props) {
const [data, setData] = useState({
id: props.id,
inputValue: ""
});
useEffect(() => {
if (props.msgPort) {
props.retrieveData(props.order, data);
}
}, [props.msgPort]);
return (
<div className="m-3 text-start p-3 border row g-0">
<div>
<span className="float-start mb-2">Component A</span>
<span className="float-end">ID: {props.id}</span>
</div>
<input
className="form-control"
type="text"
placeholder="inputValue"
value={data.inputValue}
onChange={(evt) => {
setData({ ...data, inputValue: evt.target.value });
}}
/>
</div>
);
}
const B = (props) => {
const [data, setData] = useState({
id: props.id,
checkedValue: true
});
useEffect(() => {
if (props.msgPort) {
props.retrieveData(props.order, data);
}
}, [props.msgPort]);
return (
<form className="m-3 text-start p-3 border row g-0">
<div>
<span className="float-start mb-2">Component B</span>
<span className="float-end">ID: {props.id}</span>
</div>
<div className="form-check">
<input
className="form-check-input"
type="checkbox"
checked={data.checkedValue}
onChange={() => {
setData({ ...data, checkedValue: !data.checkedValue });
}}
/>
<label className="form-check-label">Default checkbox</label>
</div>
</form>
);
};
export default function App() {
const [msgPort, setMsgPort] = useState("");
const [appData, setAppData] = useState([]);
const saveData = () => {
setAppData(["", "", ""]);
setMsgPort("save"); // this will trigger the retrieve data in the children components
};
const retrieveData = (index, componentData) => {
setAppData((prevState) => {
let newData = [...prevState];
newData[index] = componentData;
return newData;
});
setMsgPort(""); // Is there a potential for a component to not get updated before this get reset
};
return (
<div className="App m-2 p-3 border">
<h1 className="h2">Children</h1>
<div className="p-3 m-2 border bg-light">
<A id={1} order={0} msgPort={msgPort} retrieveData={retrieveData} />
<B id={2} order={1} msgPort={msgPort} retrieveData={retrieveData} />
<A id={3} order={2} msgPort={msgPort} retrieveData={retrieveData} />
</div>
<button
type="button"
className="btn btn-light btn-outline-dark"
onClick={() => {
saveData();
}}
>
Get Children Data
</button>
{Object.keys(appData).length > 0 && (
<div className="my-3">
<label className="form-label">Template Data</label>
<span className="form-control-plaintext">
{JSON.stringify(appData)}
</span>
</div>
)}
</div>
);
}
I suggest creating a React context to hold the state you want to "retrieve" from the children, exposing out only a single callback for them to call and pass their state up. This inverts control where the parent component doesn't need to be aware of its children and by using a React Context you don't need to drill all the state and callbacks as props through to the children. It's an opt-in system where children components need only use the useSaveState hook to send their state to the centralized context value.
Example:
import { createContext, useContext, useEffect, useState } from "react";
const SaveStateContext = createContext({
saveState: () => {}
});
const useSaveState = (key) => {
const { saveState } = useContext(SaveStateContext);
const saveStateWithKey = (value) => saveState(key, value);
return { saveState: saveStateWithKey };
};
const SaveStateProvider = ({ children }) => {
const [state, setState] = useState({});
useEffect(() => {
... any side-effect with the updated states
}, [state]);
const saveState = (key, value) =>
setState((state) => ({
...state,
[key]: value
}));
const value = useMemo(() => ({ saveState }), []);
return (
<SaveStateContext.Provider value={value}>
{children}
</SaveStateContext.Provider>
);
};
Usage:
const { saveState } = useSaveState(id);
const [data, setData] = useState( ... );
useEffect(() => {
saveState(data);
}, [data, saveState]);
Related
I don't understand why my page can't recognize other pages when I click (for example on page 2, the same page appears again and again)
This is in MealNew.js component:
import React, {useEffect, useState } from "react";
import './MealNew.css';
import Card from "../UI/Card";
import AppPagination from "./AppPagination";
const MealNew = () => {
const [data, setData] = useState([]);
const [showData, setShowData] = useState(false);
const [query,setQuery] = useState('');
const[page,setPage] = useState(9);
const[numberOfPages,setNumberOfPages]= useState(10);
const handleClick = () => {
setShowData(true);
const link = `https://api.spoonacular.com/recipes/complexSearch?query=${query}&apiKey=991fbfc719c743a5896bebbd98dfe996&page=${page}`;
fetch (link)
.then ((response)=> response.json())
.then ((data) => {
setData(data.results)
setNumberOfPages(data.total_pages)
const elementFood = data?.map((meal,key) => {
return (<div key={key}>
<h1>{meal.title}</h1>
<img src={meal.image}
alt='e-meal'/>
</div> )
})
const handleSubmit = (e) => {
e.preventDefault();
handleClick();
}
useEffect(()=> {
handleClick();
},[page])
return (
<Card className="meal">
<form onSubmit={handleSubmit}>
<input
className="search"
placeholder="Search..."
value={query}
onChange={(e)=>setQuery(e.target.value)}/>
<input type='submit' value='Search'/>
</form>
<li className="meal">
<div className = 'meal-text'>
<h5>{showData && elementFood}</h5>
<AppPagination
setPage={setPage}
pageNumber={numberOfPages}
/>
</div>
</li>
</Card>
) }
export default MealNew;
This is in AppPagination.js component:
import React from "react";
import { Pagination } from "#mui/material";
const AppPagination = ({setPage,pageNumber}) => {
const handleChange = (page)=> {
setPage(page)
window.scroll(0,0)
console.log (page)
}
return (
<div >
<div >
<Pagination
onChange={(e)=>handleChange(e.target.textContent)}
variant="outlined"
count={pageNumber}/>
</div>
</div>
)
}
export default AppPagination;
Thanks in advance, I would appreciate it a lot
The only error I am getting in Console is this:
Line 64:3: React Hook useEffect has a missing dependency: 'handleClick'. Either include it or remove the dependency array react-hooks/exhaustive-deps
You are not following the spoonacular api.
Your link looks like this:
https://api.spoonacular.com/recipes/complexSearch?query=${query}&apiKey=<API_KEY>&page=${page}
I checked the spoonacular Search Recipes Api and there's no page parameter you can pass. You have to used number instead of page.
When you receive response from the api, it returns the following keys: offset, number, results and totalResults.
You are storing totalResults as totalNumberOfPages in state which is wrong. MUI Pagination count takes total number of pages not the total number of records. You can calculate the total number of pages by:
Math.ceil(totalRecords / recordsPerPage). Let say you want to display 10 records per page and you have total 105 records.
Total No. of Pages = Math.ceil(105/10)= 11
Also i pass page as prop to AppPagination component to make it as controlled component.
Follow the documentation:
Search Recipes
Pagination API
Complete Code
import { useEffect, useState } from "react";
import { Card, Pagination } from "#mui/material";
const RECORDS_PER_PAGE = 10;
const MealNew = () => {
const [data, setData] = useState([]);
const [showData, setShowData] = useState(false);
const [query, setQuery] = useState("");
const [page, setPage] = useState(1);
const [numberOfPages, setNumberOfPages] = useState();
const handleClick = () => {
setShowData(true);
const link = `https://api.spoonacular.com/recipes/complexSearch?query=${query}&apiKey=<API_KEY>&number=${page}`;
fetch(link)
.then((response) => response.json())
.then((data) => {
setData(data.results);
const totalPages = Math.ceil(data.totalResults / RECORDS_PER_PAGE);
setNumberOfPages(totalPages);
});
};
const elementFood = data?.map((meal, key) => {
return (
<div key={key}>
<h1>{meal.title}</h1>
<img src={meal.image} alt='e-meal' />
</div>
);
});
const handleSubmit = (e) => {
e.preventDefault();
handleClick();
};
useEffect(() => {
handleClick();
console.log("first");
}, [page]);
return (
<Card className='meal'>
<form onSubmit={handleSubmit}>
<input className='search' placeholder='Search...' value={query} onChange={(e) => setQuery(e.target.value)} />
<input type='submit' value='Search' />
</form>
<li className='meal'>
<div className='meal-text'>
<h5>{showData && elementFood}</h5>
<AppPagination setPage={setPage} pageNumber={numberOfPages} page={page} />
</div>
</li>
</Card>
);
};
const AppPagination = ({ setPage, pageNumber, page }) => {
const handleChange = (page) => {
setPage(page);
window.scroll(0, 0);
console.log(page);
};
console.log("numberOfPages", pageNumber);
return (
<div>
<div>
<Pagination
page={page}
onChange={(e) => handleChange(e.target.textContent)}
variant='outlined'
count={pageNumber}
/>
</div>
</div>
);
};
export default MealNew;
I am trying to make a flashcard web app for language learning and/or rote learning. I have managed to show the first element of the array which contains the data that I'm fetching from the backend but I can't switch from the first element to the subsequent elements.
Here is my code in React:
// Decklist component that displays the flashcard
import { React, useEffect, useState, useContext } from "react";
import Card from "./Card";
import cardContext from "../store/cardContext";
const axios = require("axios");
export default function Decklist() {
//State for data fetched from db
const [data, setData] = useState([]);
//State for array element to be displayed from the "data" state
const [position, setPosition] = useState(0);
//function to change the array element to be displayed after user reads card
const setVisibility = () => {
setPosition(position++);
};
//function to change the difficulty of a card
const difficultyHandler = (difficulty, id) => {
console.log(difficulty);
setData(
data.map((ele) => {
if (ele.ID === id) {
return { ...ele, type: difficulty };
}
return ele;
})
);
};
//useEffect for fetching data from db
useEffect(() => {
axios
.get("/api/cards")
.then((res) => {
if (res.data) {
console.log(res.data);
setData(res.data.sort(() => (Math.random() > 0.5 ? 1 : -1)));
}
})
.catch((err) => {
console.log(err);
});
}, []);
return (
<cardContext.Provider
value={{ cardData: data, setDifficulty: difficultyHandler }}
>
{data.length && (
<Card
position={position}
// dataIndex={index}
visible={setVisibility}
id={data[position].ID}
front={data[position].Front}
back={data[position].Back}
/>
)}
</cardContext.Provider>
);
}
//Card component
import { React, useState, useEffect } from "react";
import Options from "./Options";
export default function Card(props) {
//State for showing or hiding the answer
const [reverse, setReverse] = useState(false);
const [display, setDisplay] = useState(true);
//function for showing the answer
const reversalHandler = () => {
setReverse(true);
};
return (
<div>
{reverse ? (
<div className="card">
{props.front} {props.back}
<button
onClick={() => {
props.visible();
}}
>
Next Card
</button>
</div>
) : (
<div className="card">{props.front}</div>
)}
<Options
visible={props.visible}
reverse={reversalHandler}
id={props.id}
/>
</div>
);
}
//Options Component
import { React, useContext, useState } from "react";
import cardContext from "../store/cardContext";
export default function Options(props) {
const ctx = useContext(cardContext);
const [display, setDisplay] = useState(true);
return (
<>
<div className={display ? "" : "inactive"}>
<button
onClick={() => {
setDisplay(false);
props.reverse();
ctx.setDifficulty("easy", props.id);
}}
>
Easy
</button>
<button
onClick={() => {
setDisplay(false);
props.reverse();
ctx.setDifficulty("medium", props.id);
}}
>
Medium
</button>
<button
onClick={() => {
setDisplay(false);
props.reverse();
ctx.setDifficulty("hard", props.id);
}}
>
Hard
</button>
</div>
</>
);
}
The setVisibility function in the Decklist component is working fine and setting the position state properly. However, I don't know how to re-render the Card component so that it acts on the position state that has changed.
One way to force a re-render of a component is to set its state to itself
onClick={() => {
props.visible();
setReverse(reverse);
}}
However this probably isn't your issue as components will automatically re-render when their state changes or a parent re-renders. This means that for some reason the Card component isn't actually changing the parent component.
Im triying to remove this warning on a react component
Line 19:8: React Hook useEffect has a missing dependency: 'handleChange'. Either include it or remove the dependency array react-hooks/exhaustive-deps
this is the component
const SelectButton = (props)=>{
const [activeState, setActiveState] = useState(false)
const label = props.label
const handleClick = () =>{
setActiveState(!activeState)
//props.handleChange(label,activeState)
}
const handleChange = props.handleChange
useEffect(()=>{
handleChange(label,activeState)
}, [label,activeState])
return(
<button
type="button"
onClick={handleClick}
className={"container-form-button "+(activeState?"active":"")}>
{label}
</button>
)
}
if i tried to remove the comments on handleChange inside of handleClick, handleChange didn´t works correctly
if i tried to change useEffect for something like this
useEffect(()=>{
handleChange(label,activeState)
}, [label,activeState,handleChange])
or
useEffect(()=>{
props.handleChange(label,activeState)
}, [label,activeState,props.handleChange])
it try to reder to many times and throw this error.
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render
Actually it works like first code, but im still having the warning
this is the parent original handleChange
const handleChange = (name,value)=>{
setSelected({...selected, [name]:value})
}
the parentComponent
const CategorySearcher = (props) =>{
const ciudades = ["Guadalajara","Zapopan","Tlajomulco"]
const tipos = ["Casa","Departamento"]
const [selected, setSelected] = useState({})
const handleChange = useCallback(
(label,value) => {
setSelected({...selected, [label]:value})
},
[selected],
)
useEffect(() => {
console.log(selected);
}, [selected])
const cities = ciudades.map((city)=><SelectButton key={city} handleChange={handleChange} label={city}/>)
const types = tipos.map((tipo)=><SelectButton key={tipo} handleChange={handleChange} label={tipo}/>)
let history = useHistory();
const setUrlSearch = ()=>{
let urlSearch = "search/q="
let attToSearch = []
for (var key in selected) {
selected[key]?attToSearch.push(key):console.log("Nada")
}
/*
attToSearch.forEach((it)=>{
urlSearch = urlSearch+"&"+it
})*/
console.log(urlSearch);
history.push(urlSearch+attToSearch)
}
return (
<section className="general-container">
<div className="container-box">
<span className="container_title">
Tu nuevo hogar esta aquí :)
</span>
</div>
<div className="container-form">
<form className="container-form-box">
<div className="container-form-cities">
<div className="container-form-subtitle">
Ciudades
</div>
<div className="container-buttons">
{cities}
</div>
</div>
<div className="container-form-cities">
<div className="container-form-subtitle">Tipo de hogar</div>
<div className="container-buttons">
{types}
</div>
</div>
<div className="container-box-button">
<button className="button-form-CTA" onClick={setUrlSearch}>
Buscar
</button>
</div>
</form>
</div>
</section>
)
}
You should be wrapping your function with the useCallback hook before passing it as a prop. Documentation can be found here.
You shouldn't be using useEffect like that.
const SelectButton = ({ label, handleChange }) => {
const [activeState, setActiveState] = React.useState(false);
const handleClick = () => {
const newState = !activeState
setActiveState(newState);
handleChange(label, newState);
};
return (
<button
type="button"
onClick={handleClick}
className={"container-form-button " + (activeState ? "active" : "")}
>
{label}
</button>
);
};
Why don't you destructure label and handleChange from props? Check out this sandbox, this does not cause any infinite update loop: codesandbox
import React from "react";
export default function App() {
const handleChange = () => console.log("handling change");
return (
<div className="App">
<SelectButton label="Label" handleChange={handleChange} />
</div>
);
}
const SelectButton = ({ label, handleChange }) => {
const [activeState, setActiveState] = React.useState(false);
const handleClick = () => {
setActiveState(!activeState);
};
React.useEffect(() => {
handleChange(label, activeState);
}, [label, activeState, handleChange]);
return (
<button
type="button"
onClick={handleClick}
className={"container-form-button " + (activeState ? "active" : "")}
>
{label}
</button>
);
};
I have been looking on google a lot about how to pass props between functional components but very little information seems to be out there(either that or I don't know what keywords to input into google).
I do not need redux or data stored globally, I simply want to pass a JSON object stored in a hook from one component file to another.
I have three files one is the parent and the other two are children, I want to pass the data between the children files.
Paerent
import React, { useState } from "react";
import ShoppingPageOne from "./ShoppingPageOne";
import ShoppingPageTwo from "./ShoppingPageSecond";
function ShoppingPageContainer() {
const [pageone_Open, setPageone_Open] = useState(true);
const [pagetwo_Open, setPagetwo_Open] = useState(false);
const page_showHandler = () => {
setPageone_Open(!pageone_Open);
setPagetwo_Open(!pagetwo_Open);
};
return (
<div className="Shopping_Container">
<div className="Shopping_Box">
<h2>Online food shop</h2>
<div className="Shopping_Page_Container">
<ShoppingPageOne showOne={pageone_Open} next_ClickHandler={page_showHandler} />
<ShoppingPageTwo showTwo={pagetwo_Open} Reset_Data={page_showHandler} />
</div>
</div>
</div>
);
}
export default ShoppingPageContainer;
Child one:
import React, { useState, useEffect } from "react";
import Data from '../shoppingData/Ingredients';
const ShoppingPageOne = (props) => {
//element displays
const [pageone_show, setPageone_show] = useState("pageOne");
//stores quantities of items as JSON objects
const [Quantities, setQuantities] = useState({});
const [QuantiesProps, setQuantitiesProps] = useState(null)
useEffect(() => {
//sets info text using Json
if (props.showOne) {
setPageone_show("pageOne");
} else {
setPageone_show("pageOne hide");
}
}, [props.showOne]);
return (
<div className={"Shopping_Content " + pageone_show}>
{Data.map((Ingredients) => {
//updates Quanties Hook
const handleChange = (event) => {
setQuantities({
...Quantities,
[Ingredients.Name]: {
...(Quantities[Ingredients.Name] ?? {}),
quantities: event.target.value
}
});
};
return (<div className="Shopping_input" key={Ingredients.Name}>
<p>{Ingredients.Name} £{Ingredients.Price}</p>
<input onChange={handleChange.bind(this)} min="0" type="number"></input>
</div>)
})}
<div className="Shopping_Buttons">
<p onClick={props.next_ClickHandler}>Buy Now!</p>
</div>
</div>
);
};
export default ShoppingPageOne;
Child Two
import React, { useState, useEffect } from "react";
import Data from '../shoppingData/Ingredients';
const ShoppingPageSecond = (props) => {
//element displays
const [pagetwo_show, setPagetwo_show] = useState("pageTwo hide");
useEffect(() => {
//resets info text
if (props.showTwo) {
setPagetwo_show("pageTwo");
} else {
setPagetwo_show("pageTwo hide");
}
}, [props.showTwo]);
return (
<div className={"Shopping_Content " + pagetwo_show}>
<div className="Shopping_Buttons">
<p onClick={props.Reset_Data}>Shop Again</p>
</div>
</div>
);
};
export default ShoppingPageSecond;import React, { useState, useEffect } from "react";
import Data from '../shoppingData/Ingredients';
const ShoppingPageSecond = (props) => {
//element displays
const [pagetwo_show, setPagetwo_show] = useState("pageTwo hide");
useEffect(() => {
//resets info text
if (props.showTwo) {
setPagetwo_show("pageTwo");
} else {
setPagetwo_show("pageTwo hide");
}
}, [props.showTwo]);
return (
<div className={"Shopping_Content " + pagetwo_show}>
<div className="Shopping_Buttons">
<p onClick={props.Reset_Data}>Shop Again</p>
</div>
</div>
);
};
export default ShoppingPageSecond;
I simply want to pass the state contained in Quantities hook from Child One to Child Two when "Buy Now!" button is clicked.
What is the best approach to do doing this?
From my understand, I don't pass props between two children under the same parent. Instead, the parent holds the data, and pass the data and mutation function to children as props.
import React, { useState } from 'react';
const PageOne = ({ value, setValue }) => {
const PageOneFunction = () => {
setValue({
pageOneData: value.pageOneData + 1,
pageTwoData: value.pageTwoData + 1,
});
};
return (
<div>
<h4>Page One</h4>
<div>{value.pageOneData}</div>
<button onClick={PageOneFunction}>
Increase page one and page two value
</button>
</div>
);
};
const PageTwo = ({ value, setValue }) => {
const pageTwoFunction = () => {
setValue({
pageOneData: 0,
pageTwoData: 0,
});
};
return (
<div>
<h4>Page Two</h4>
<div>{value.pageTwoData}</div>
<button onClick={pageTwoFunction}>Reset</button>
</div>
);
};
const PageContainer = () => {
const [data, setData] = useState({
pageOneData: 0,
pageTwoData: 0,
});
return (
<div className="bg-white">
<PageOne value={data} setValue={setData} />
<PageTwo value={data} setValue={setData} />
</div>
);
};
export default PageContainer;
I have a question on React Hooks. This is a sample of my code :-
import React, { useState, useEffect } from "react";
import Card from "./Card";
const CardsBoard = () => {
useEffect(() => {
doRatingClickProcessing()
}, [ratingObj])
const doRatingClickProcessing = () => {
const { player, title, rating } = ratingObj
}
return (
<div className="container-fluid justify-content-center">
<div className="row">
<div className="col-md-6">
<Card
cardInfo={player1Card}
player={1}
showCard={visiblePl1}
clickableRatings = {clickableRatings}
onClick={ratingObj => setRatingObj(ratingObj)}
/>
</div>
<div className="col-md-6">
<Card
cardInfo={player2Card}
player={2}
showCard={visiblePl2}
clickableRatings = {clickableRatings}
onClick={ratingObj => setRatingObj(ratingObj)}
/>
</div>
</div>
)}
</div>
)
}
export default CardsBoard
Then in the card component I am returning the ratingObj successfully when the user clicks on a rating.
In the Card Component I have something like this:-
<div
className="col-md-2 text-left card-rating-color"
onClick={() =>
onClick({
player: player,
title: row[0].title,
rating: row[0].rating,
})
}
>
{row[0].rating}
</div>
However I am puzzled why useEffect() is triggered even when the Card component is loaded, and ratingObj is still empty. Shouldn't it be triggered only if the ratingObj is filled up?
Thanks for your help and time
useEffect will call at least once. it doesn't matter either your object is updating or not because when you write
useEffect(()=>{
},[ratingObj]);
In above code you are passing object into square brackets right. That means you are mentioning dependencies as e second parameter and empty [] in argument list will call once at least. After that, it depends on ratingObj that you have passed in.
import React, {useState,useMemo} from 'react';
const App = () => {
const [name, setName] = useState('');
const [modifiedName, setModifiedName] = useState('');
const handleOnChange = (event) => {
setName(event.target.value);
}
const handleSubmit = () => {
setModifiedName(name);
}
const titleName = useMemo(()=>{
console.log('hola');
return `${modifiedName} is a Software Engineer`;
},[modifiedName]);
return (
<div>
<input type="text" value={name} onChange={handleOnChange} />
<button type="button" onClick={handleSubmit}>Submit</button>
<Title name={titleName} />
</div>
);
};
export default App;
const Title = ({name}) => {
return <h1>{name}</h1>
}