How to share context sub files - nextjs - javascript

This is my pages:
/pages
/gift
/[slug]
index.tsx
/personalize
index.tsx
I have a GiftProvider inside /gift/[slug]/index.tsx:
return (
<GiftProvider gift={gift}>
<ProductPage />
user can see /personalize/index.tsx inside ProductPage component with a function:
const goToPersonalize = () => {
router.push(`/gift/${gift.id}/personalize`)
}
....
now I have a gift state inside /gift/[slug]/index.tsx. (init this with SSG)
How I can access to this state inside personalize?

I would separate GiftProvider from the page, and expose a custom hook for updating/reading the state.
Ex. ./components/giftProvider
const GiftContext= React.createContext(null)
export function GiftProvider ({ children }) {
const [gift,setGift] = useState(null)
return (
<GiftContext.Provider value={{gift,setGift}} >
{children}
</GiftContext.Provider>
)
}
export function useGift() {
const value = useContext(AuthContext)
return value
}
Now wrap every page inside GiftProvider :
_app.js
import { GiftProvider } from './components/giftProvider'
function MyApp({ Component, pageProps }) {
return (
<GiftProvider>
<Component {...pageProps} />
</GiftProvider>
)
}
export default MyApp
Then you can set the gift using your custom hook whereever you need to :
import {useGift} from '../../components/giftProvider '
....
const {gift,setGift} = useGift()
let mygift = "something" // the gift object
setGift(mygift )
....
In the same way you can get current gift (ex. inside /personalize/index.tsx):
import {useGift} from '../../components/giftProvider '
....
const {gift} = useGift()
....
If you want to persist your state (ex. store gift in local storage), you can change setGift function to store data, and add useEffect inside giftProvider to determine if there is already a gift stored.
Something like this :
export function GiftProvider ({ children }) {
const [gift,setGift] = useState(null)
useEffect(()=>{
let storedGift = localStorage.getItem('gift') ? localStorage.getItem('gift') : {} ;
setGift(JSON.parse(storedGift)
},[])
const setNewGift = (giftObject) => {
localStorage.setItem('gift', JSON.stringify(giftObject));
setGift(giftObject)
}
return (
<GiftContext.Provider value={{gift,setGift,setNewGift}} >
{children}
</GiftContext.Provider>
)
}

Related

Prevent losing data when refreshing on a different route - react

I wanted to prevent losing state on page refresh while being on a different route path. Im curious why the first example does not work. From what i understand when app mounts first thing that gonna render is component itself and then useEffects run. Since i got 3 here, first fetches and saves the data to the invoiceList state and then next useEffect that run should fill localStorage key with invoiceList state data. The last one obviously retrieve the data.
The second one does fill the "invoiceData" localStorage key with an empty array. Why is this happening if the invoiceList state already have the data after the first useEffect?
The second example that i provided works. I removed second useEffect and set localStorage key in the first useEffect with response data that i get from fetch.
I also wonder if im doing everything correct here. Any feedback appreciated :)
First example (not working):
import { ReactElement, useEffect, useState } from "react";
import { Outlet } from "react-router-dom";
import { Bar } from "../../components/Bar/Bar";
import { Invoice } from "./Root.utils";
type Props = {};
const Root = (props: Props): ReactElement => {
const [invoiceList, setInvoiceList] = useState<Invoice[]>([]);
useEffect(() => {
const fetchData = async () => {
const response = await fetch("./data.json");
const data = await response.json();
setInvoiceList(data);
};
fetchData();
}, []);
useEffect(() => {
window.localStorage.setItem("invoiceData", JSON.stringify(invoiceList));
}, []);
useEffect(() => {
setInvoiceList(
JSON.parse(window.localStorage.getItem("invoiceData") || "[]")
);
}, []);
return (
<div>
<Bar />
<Outlet context={{ invoiceList }} />
</div>
);
};
export default Root;
Second example (working):
import { ReactElement, useEffect, useState } from "react";
import { Outlet } from "react-router-dom";
import { Bar } from "../../components/Bar/Bar";
import { Invoice } from "./Root.utils";
type Props = {};
const Root = (props: Props): ReactElement => {
const [invoiceList, setInvoiceList] = useState<Invoice[]>([]);
useEffect(() => {
const fetchData = async () => {
const response = await fetch("./data.json");
const data = await response.json();
window.localStorage.setItem("invoiceData", JSON.stringify(data));
setInvoiceList(data);
};
fetchData();
}, []);
useEffect(() => {
setInvoiceList(
JSON.parse(window.localStorage.getItem("invoiceData") || "[]")
);
}, []);
return (
<div>
<Bar />
<Outlet context={{ invoiceList }} />
</div>
);
};
export default Root;
The first example is never storing the data into the localStorage because the fetch is an asynchronous function that and you are writing basically always the empty array into your localStorage.
The order of execution in the first example will be:
fetchData called
window.localStorage.setItem("invoiceData", JSON.stringify(invoiceList)); <- still empty array
setInvoiceList(JSON.parse(window.localStorage.getItem("invoiceData") || "[]"));
response.json() called
setInvoiceList(data); called
I would also recommend to improve your code a little like that:
import React, { useEffect, useState } from "react";
import { Outlet } from "react-router-dom";
import { Bar } from "../../components/Bar/Bar";
import { Invoice } from "./Root.utils";
const Root: React.FC = () => {
const [invoiceList, setInvoiceList] = useState<Invoice[]>([]);
useEffect(() => {
setInvoiceList(
JSON.parse(window.localStorage.getItem("invoiceData") || "[]")
);
const fetchData = async () => {
const response = await fetch("./data.json");
const data = await response.json();
window.localStorage.setItem("invoiceData", JSON.stringify(data));
setInvoiceList(data);
};
fetchData();
}, []);
return (
<div>
<Bar />
<Outlet context={{ invoiceList }} />
</div>
);
};
export default Root;
You can use the Link component from react-router and specify to={} as an object where you specify pathname as the route to go to. Then add a variable e.g. data to hold the value you want to pass on. See the example below.
Using the <Link /> component:
<Link
to={{
pathname: "/page",
state: data // your data array of objects
}}
>
Using history.push()
this.props.history.push({
pathname: '/page',
state: data // your data array of objects
})
Using either of the above options you can now access data on the location object as per the below in your page component.
render() {
const { state } = this.props.location
return (
// render logic here
)
}

How to receive data through props React and send it back?

I'm getting a placeholder value through props in my input component and I need to send the input value back to the main class. I'm using React but I'm not getting it. Follow my code.... The value I need to send is the value of 'usuario'
import React, { useState } from 'react';
import { EntradaDados } from './styled';
const PesqDados = ({placeholder, usuario}) => {
const [usuario, SetUsuario] = useState('')
const setValor =(e)=>{
SetUsuario(e.target.value);
}
console.log(usuario);
return(
<EntradaDados
onChange={setValor}
placeholder={placeholder}
>
</EntradaDados>
);
}
export default PesqDados;
You need to add a callback prop (onUsuarioChange) to your PesqDados component and call it with the new usuario. You have two options:
Call it from a useEffect with usuario as dependency (assuming usuario could get updated from somewhere other than setValor.
Call it from setValor, assuming that's the only place where usuario is going to get updated from.
This is how this should look:
import React, { useState } from 'react';
import { EntradaDados } from './styled';
const PesqDados = ({
placeholder,
usuario,
onUsuarioChange
}) => {
const [usuario, setUsuario] = useState('');
// Option 1:
useEffect(() => {
onUsuarioChange(usuario);
}, [usuario]);
const setValor = (e) => {
const nextUsuario = e.target.value;
setUsuario(nextUsuario);
// Option 2:
onUsuarioChange(nextUsuario);
};
return (
<EntradaDados
onChange={ setValor }
placeholder={ placeholder } />
);
}
export default PesqDados;
After studying properly, I found that I don't need to implement the function in the component page. I just needed to create a hook that calls the component's OnChange property on the component's page and then create a function just in the place where the component is installed. In this case, App.js.
Page Component....
const PesqDados = ({placeholder, Dados}) => {
return(
<EntradaDados
onChange={Dados}
placeholder={placeholder}
>
</EntradaDados>
);
}
export default PesqDados;
Page App.js
function App() {
const [usuario, SetUsuario] = useState('Aguardando Dados...')
const setValor =(e)=>{
SetUsuario(e.target.value);
}
const teste = ()=>{
alert("O usuário digitado foi : "+usuario)
};
return (
<>
<div className='divRepos'>
<div className='bloco'>
<div className='pesquisar'>
<p>{usuario}</p>
<PesqDados
placeholder={"Digite um username válido"}
Dados={setValor}
/>
<Button nomeBotao={"Pesquisar Perfil"}
onClick={teste}/>
</div>
...

React hooks: Dynamically mapped component children and state independent from parent

I am gathering posts (called latestFeed) from my backend with an API call. These posts are all mapped to components and have comments. The comments need to be opened and closed independently of each other. I'm governing this mechanic by assigning a piece of state called showComment to each comment. showComment is generated at the parent level as dictated by the Rules of Hooks.
Here is the parent component.
import React, { useState, useEffect } from "react";
import { getLatestFeed } from "../services/axios";
import Child from "./Child";
const Parent= () => {
const [latestFeed, setLatestFeed] = useState("loading");
const [showComment, setShowComment] = useState(false);
useEffect(async () => {
const newLatestFeed = await getLatestFeed(page);
setLatestFeed(newLatestFeed);
}, []);
const handleComment = () => {
showComment ? setShowComment(false) : setShowComment(true);
};
return (
<div className="dashboardWrapper">
<Child posts={latestFeed} showComment={showComment} handleComment={handleComment} />
</div>
);
};
export default Parent;
latestFeed is constructed along with showComment. After latestFeed comes back with an array of posts in the useEffect hook, it is passed to the child show here:
import React, { useState } from "react";
const RenderText = ({ post, showComment, handleComment }) => {
return (
<div key={post._id} className="postWrapper">
<p>{post.title}</p>
<p>{post.body}</p>
<Comments id={post._id} showComment={showComment} handleComment={() => handleComment(post)} />
</div>
);
};
const Child = ({ posts, showComment, handleComment }) => {
return (
<div>
{posts.map((post) => {
<RenderPosts posts={posts} showComment={showComment} handleComment={handleComment} />;
})}
</div>
);
};
export default Child;
However, whenever I trigger handleComments, all comments open for all posts. I'd like them to be only the comment that was clicked.
Thanks!
You're attempting to use a single state where you claim you want multiple independent states. Define the state directly where you need it.
In order to do that, remove
const [showComment, setShowComment] = useState(false);
const handleComment = () => {
showComment ? setShowComment(false) : setShowComment(true);
};
from Parent, remove the showComment and handleComment props from Child and RenderText, then add
const [showComment, handleComment] = useReducer(state => !state, false);
to RenderText.

How do I call a function from component that is not a child nor parent?

Component A
const query = ....
getData(query);
Component B
//here I want to call it using component's A argument
You would probably have a parent around these two components. You can use the parent to share the function:
function Parent() {
const [ dataWithQuery, setDataWithQuery ] = useState(null);
return (
<>
<ComponentA setDataWithQuery={ setDataWithQuery } />
<ComponentB dataWithQuery={ dataWithQuery } />
</>
);
}
and in Component A you'd have:
function ComponentA({ setDataWithQuery }) {
const yourQueryHere = '...'
function getData(query) { ... }
useEffect(() => {
setDataWithQuery(() => getData(yourQueryHere));
}, []);
...
}
Notice the line where I setDataWithQuery(...), I created a new function that calls getData with your query variable. This is how you save the function parameter to be used outside of ComponentA.
and in Component B you'd have:
function ComponentB({ dataWithQuery }) {
useEffect(() => {
if(dataWithQuery != null) {
const data = dataWithQuery();
}
}, [ dataWithQuery ]);
...
}
But there is no real reason to structure like this. Why not pass up the query from ComponentA to a parent, get the data from the parent using that query, then pass that data to ComponentB?
Edit: you could also look up React Contexts for sharing without passing up and down parent/child. There would still need to be a parent around.
Do you mean, you want to pass a query down to ComponentA, get the data from within it, then use that data in ComponentB?
If that is not what you want and you want to use the same query in each one, then just keep the query parameter in the component above them post and pass them down in props.
If the former is what you want:
Parent Component:
import "./styles.css";
import ComponentA from "./ComponentA";
import ComponentB from "./ComponentB";
import { useState } from "react";
export default function App() {
const [query, setQuery] = useState("get some data");
const [data, setData] = useState({});
return (
<div className="App">
<ComponentA query={query} setData={setData} />
<ComponentB data={data} />
</div>
);
Component A
import React, { useEffect } from "react";
function ComponentA({ query, setData }) {
useEffect(() => {
if (query !== "") {
getData(query);
}
}, [query]);
async function getData(query) {
//do some stuff
const result = { id: "1", message: "I am data" };
setData(result);
}
return (
<div>
<h1>I am component A</h1>
</div>
);
}
export default ComponentA;
ComponentB
function ComponentB({ data }) {
function doSomethingWithComponentAData() {
//do stuff with the data from componentA
}
return (
<div>
<h1>I am Component B, I got data: {data.id}</h1>
</div>
);
}
export default ComponentB;
Try this:
// Component A
export const getData = () => { // ... }
// Component B
import { getData } from './ComponentA'
const result = getData()

React functional component - how to count instances?

I need to be able to track number of instances of my component, how do I do that in React using functional components?
I tried to use useRef() but it seems like even though it preserves the value between the renders - it does not share the value between the component instances.
So far the only solution I came up with is this sily one, I hope there is a way to store it somehow in more elegant way.
const [ ident, setIdent ] = useState(0);
useEffect(() => {
if (document.sometring === undefined) {
document.sometring = 0;
} else {
document.sometring++;
}
setIdent(document.sometring);
}, []);
Update to the question:
The use case is more actademical, I want to know how to do it, rather than practical. I want every instance of my independent component to have unique sequential ID (like "button-42") so this is why solutions like "give it a random code" also won't work for me. Global state managers like redux or context also cannot be a solution because, let's say, If i open-source my component on GitHub I should not ask users to install also redux or use React.Context. And of course this ID should not change if component re-renders.
You can use the initialise function of useState or with useEffect (if you don't need the updated value in the component) to increment the counter, and set the initialState to the new value:
/** export **/ const count = { val: 0 };
const Comp = ({ force }) => {
// if you don't need the value inside the component on render, you can replace with useEffect(() => (count.val++, count.val), [])
const [id] = React.useState(() => ++count.val);
return <div>{force} Count {id}</div>;
}
const Demo = () => {
const [force, setForce] = React.useState(0);
return (
<div>
<Comp force={force} />
<Comp force={force} />
<Comp force={force} />
<button onClick={() => setForce(force + 1)}>Force Render</button>
</div>
);
}
ReactDOM.render(
<Demo />,
root
)
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
If you want to track the number of live components in the app (ignoring those that were rendered before but not anymore)
const count = { value: 0 }
export { count }
const incrementCounter = () => count.value++
const decrementCounter = () => count.value--
// ...
useEffect(() => {
incrementCounter();
return decrementCounter; // will run on "unmount"
}, []); // run this only once
Sure if you want to display this count somewhere else in the app you will need to make it reactive - for example, pass incrementCounter and decrementCounter functions as a prop and update counter somewhere in the state of your components or Redux (or leave it up to whoever is using this component)
Redux solution
Dom Output:
hi my id is 0
hi my id is 1
hi my id is 2
hi my id is 3
hi my id is 4
Total instances: 5
React Side:
SomeTrackedComponent.js
export default const SomeTrackedComponent = ({id}) => (
<div> hi my id is {id} </div>
)
App.js
const App = ({instances , addInstance}) =>{
const [trackedComponents, setTrackedComponents] = useState([]);
useEffect(()=>{
const justSomeArray = Array.from(Array(5).keys())
//wont have access to updated instance state within loop so initialize index
const someJSX = justSomeArray.map((_, id = instances.currentId )=>{
addInstance({ id , otherData: 'otherData?'})
return <SomeTrackedComponent key={id} id={id} />
})
setTrackedComponents(someJSX)
},[])
return(
<div>
{trackedComponents}
Total instances: {instances.items.length}
</div>
)
}
export default connect(
({instances})=>({instances}),
actions
)(App);
Redux Side:
actions.js
export const addInstance = (payload) =>(
{type:'CREATE_INSTANCE' , payload}
)
export const removeInstance = (payload) =>(
{type:'REMOVE_INSTANCE' , payload}
)
reducers.js
const instanceReducer = (state = { items : [] , currentId : 1} , action) => {
switch (action.type) {
case 'CREATE_INSTANCE':
return {
currentId: state.currentId + 1
items:[...state.items , action.payload],
}
case 'REMOVE_INSTANCE':
return {
...state,
items: [...state.items].filter(elm => elm.id !== action.payload.id)
}
default:
return state;
}
}
export default combineReducers({
instances: instanceReducer
})
Index.js:
// import React from 'react';
// import ReactDOM from 'react-dom';
// import { Provider } from 'react-redux';
// import { createStore } from 'redux';
// import reducers from './redux/reducers';
// import App from './components/App';
ReactDOM.render(
<Provider store={createStore(reducers)}>
<App />
</Provider>,
document.querySelector('#root')
);

Categories

Resources