So I am trying to store a global state using context to allow me to use the same state across different components.
The issue I am having is that when I set the global state in 1 component and try to access it in the other component to use the state. It appears to be null and I cannot figure out why?
The first component where I set the global state in will always be rendered before the component shown that seems to have an empty value for the global state.
GlobalStateProvider component:
import React from "react";
import { useState, useEffect } from "react";
import axios from "axios";
const defaultActivitiesState = [];
const globalStateContext = React.createContext(defaultActivitiesState);
const dispatchStateContext = React.createContext([]);
export const useGlobalState = () =>
[
React.useContext(globalStateContext),
React.useContext(dispatchStateContext)
];
const GlobalStateProvider = ({ children }) => {
const [state, dispatch] = React.useReducer((state, newValue) => (state, newValue),
defaultActivitiesState
);
return (
<globalStateContext.Provider value={state}>
<dispatchStateContext.Provider value={dispatch}>
{children}
</dispatchStateContext.Provider>
</globalStateContext.Provider>
);
}
export default GlobalStateProvider;
Component I set the global state in:
import react from "react";
import { useState, useEffect, useMemo } from "react";
import { MapContainer, TileLayer, Popup, Polyline } from "react-leaflet";
import axios from "axios";
import polyline from "#mapbox/polyline";
import MapComp from "./MapComp";
import { useGlobalState } from "./GlobalStateProvider";
function Map() {
// ------- global state
const [activities, setActivities] = useGlobalState(); // global state
//const [activities, setActivities] = useState([]);
//const [polylines, setPolylines] = useState(null); // as empty array value is still truthy
const [isLoading, setIsLoading] = useState(true);
const [mapMode, setMapMode] = useState("light");
const [mapStyle, setMapStyle] = useState(
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
);
const [showMarkers, setShowMarkers] = useState(true);
useEffect(() => {
setActivitieData();
console.log("activities after useEffect", activities)
}, []);
const polylines = useMemo(() => {
console.log("activities inside memo", activities)
console.log("activities.len =", activities.length);
if (activities.length) {
console.log("past len");
const polylineArray = [];
for (const item of activities) {
const polylineData = item.map.summary_polyline;
const activityName = item.name;
const activityType = item.type;
polylineArray.push({
positions: polyline.decode(polylineData),
name: activityName,
activityType: activityType,
});
}
setIsLoading(false);
return polylineArray;
}
return null;
}, [activities]);
const toggleMarkers = () => {
setShowMarkers((show) => !show);
};
const getActivityData = async () => {
console.log("calling")
const response = await axios.get(
"http://localhost:8800/api/"
);
return response.data;
};
const setActivitieData = async () => {
const activityData = await getActivityData();
setActivities(activityData);
console.log("Global activities state = ", activities);
};
return !isLoading && polylines ? (
<>
<div className="select-container">
<button className="toggle-markers" onClick={() => toggleMarkers()}>
Toggle Markers
</button>
</div>
<MapComp
className={`${mapMode}`}
activityData={{ polylines }}
showMarkers={showMarkers}
/>
</>
) : (
<div>
<p>Loading...</p>
</div>
);
}
export default Map;
component that has an empty value for global state:
import React from 'react';
import { useGlobalState } from './GlobalStateProvider';
function ActivityList() {
const [activities, setActivities] = useGlobalState();
let displayValues;
displayValues =
activities.map((activity) => {
return (
<div>
<p>{activity.name}</p>
<p>{activity.distance}m</p>
</div>
);
})
return (
<>
<p>Values</p>
{displayValues}
</>
);
}
export default ActivityList;
App.js:
function App() {
return (
<GlobalStateProvider>
<div className="App">
<NavBar />
<AllRoutes />
</div>
</GlobalStateProvider>
);
}
export default App;
Related
I'm developing a pokedex using pokeAPI through react, but I'm developing a feature where I can favorite pokemons and with that through a context, I can store the names of these pokemons in a global array. I've already managed to test and verify that the pokemon names are going to this "database" array inside the pokeDatabase const in my context, but my goal now is to pass this array to localstorage so that the browser recognizes these favorite pokemons instead of disappearing every time I refresh the page, my solution was to try to create a useEffect inside the context so that every time I refresh my application, this information is saved in localStorage, but without success. What better way to achieve this?
context:
import { createContext } from "react";
const CatchContext = createContext({
pokemons: null,
});
export default CatchContext;
provider
import React, { useEffect } from "react";
import CatchContext from "./Context";
const pokeDatabase = {
database: [],
};
const CatchProvider = ({ children }) => {
useEffect(() => {
const dataStorage = async () => {
await localStorage.setItem('pokemons', JSON.stringify(pokeDatabase.database));
}
dataStorage();
}, [])
return (
<CatchContext.Provider value={{ pokemons: pokeDatabase }}>
{children}
</CatchContext.Provider>
);
}
export default CatchProvider;
pageAddPokemon
import * as C from './styles';
import { useContext, useEffect, useState } from 'react';
import { useApi } from '../../hooks/useApi';
import { useNavigate, useParams } from 'react-router-dom';
import PokeInfo from '../../components/PokeInfo';
import AddCircleOutlineIcon from '#mui/icons-material/AddCircleOutline';
import DoNotDisturbOnIcon from '#mui/icons-material/DoNotDisturbOn';
import CatchContext from '../../context/Context';
const SinglePokemon = () => {
const api = useApi();
const { pokemons } = useContext(CatchContext);
const { name } = useParams();
const navigate = useNavigate();
const handleHompage = () => {
navigate('/');
}
const [loading, setLoading] = useState(false);
const [imgDatabase, setImgDatabase] = useState('');
const [infoPokemon, setInfoPokemon] = useState([]);
const [pokemonTypes, setPokemonTypes] = useState([]);
const [isCatch, setIsCatch] = useState(false);
useEffect(() => {
const singlePokemon = async () => {
const pokemon = await api.getPokemon(name);
setLoading(true);
setImgDatabase(pokemon.sprites);
setInfoPokemon(pokemon);
setPokemonTypes(pokemon.types);
setLoading(false);
console.log(pokemons.database);
}
singlePokemon();
verifyPokemonInDatabase();
}, []);
const verifyPokemonInDatabase = () => {
if (pokemons.database[infoPokemon.name]) {
return setIsCatch(true);
} else {
return setIsCatch(false);
}
}
const handleCatchAdd = async () => {
if (isCatch === false) {
if (!pokemons.database[infoPokemon.name]);
pokemons.database.push(infoPokemon.name);
setIsCatch(true);
}
}
const handleCatchRemove = async () => {
if (isCatch === true) {
if (!pokemons.database[infoPokemon.name]);
pokemons.database.splice(pokemons.database.indexOf(toString(infoPokemon.name)), 1);
setIsCatch(false);
}
}
return (
<C.Container>
<PokeInfo
name={infoPokemon.name}
/>
<C.Card>
<C.Info>
<C.Imgs>
<img src={imgDatabase.front_default} alt="" />
<img src={imgDatabase.back_default} alt="" />
</C.Imgs>
<h2 id='types'>Tipos</h2>
{pokemonTypes.map(type => {
return (
<C.Types>
<h2>{type.type.name}</h2>
</C.Types>
)
})}
{isCatch ? (
<DoNotDisturbOnIcon id='iconCatched' onClick={handleCatchRemove}/>
): <AddCircleOutlineIcon id='icon' onClick={handleCatchAdd}/>}
</C.Info>
</C.Card>
<C.Return>
<button onClick={handleHompage}>Retornar a Pokédex</button>
</C.Return>
</C.Container>
)
}
export default SinglePokemon;
I am receiving an error of "HomePage.jsx:16 Uncaught TypeError: elements.map is not a function" when trying to change the boxtitle for the array component. I have tried moving around functions and such but nothing seems to work.
I basically only want to change the title for the certain array object with the same boxid. Below is my code.
HomePage.jsx:
import react from 'react';
import { useEffect, useContext } from 'react';
import '../App.css';
import Todobox from './Todobox';
import { ElementContext } from '../ElementContext';
export default function HomePage(){
const { elements, setElements, newElement, elementId } = useContext(ElementContext);
return(
<div className='page-container'>
<div className='header'>
<a className='header-title'>Trello Clone!</a>
<a className='header-button' onClick={newElement}>Create a list</a>
</div>
<div className='element-field'>
{elements.map((element) => <Todobox key={element.boxid} boxid={element.boxid} boxtitle={element.boxtitle}/>)}
</div>
</div>
)
}
Todobox.jsx:
import React from 'react';
import Item from './Item';
import { useState, useContext } from 'react';
import '../App.css';
import { ElementContext } from '../ElementContext';
export default function Todobox({ boxtitle, boxid }){
const { elements, setElements } = useContext(ElementContext);
const [boxheader, setBoxHeader] = useState();
const handleSubmit = (e) => {
const object = elements.find(obj => {
if (obj.boxid === boxid){
setBoxHeader(e.target.value)
return obj
}})
setElements({...object, boxtitle: boxheader})
}
const handleKeydown = (e) => {
if(e.keyCode == 13 && e.shiftKey == false){
e.preventDefault();
handleSubmit(e)
}
}
return(
<div className='element-box'>
<textarea className='element-title-input' placeholder={boxtitle} onKeyDown={handleKeydown}/>
<Item />
<textarea
className='element-input'
type='text'
placeholder={`Add item... ${boxid}`}
onClick={() => {console.log(boxid)}}
/>
</div>
)
}
ElementContext.js:
import React, { createContext, useState } from 'react';
import Todobox from './components/Todobox';
export const ElementContext = createContext();
export const ElementContextProvider = ({children}) => {
const [elements, setElements] = useState([]);
const [elementId, setElementId] = useState(1);
const [title, setTitle] = useState('Add title...');
const [refDict, setRefDict] = useState({});
const newElementId = (elements) =>{
setElementId(elementId + 1);
console.log(elementId)
}
const newElement = () => {
newElementId();
if (!refDict[elementId]) {
setElements(prev => [...prev, { boxtitle: title, boxid: elementId }]);
setRefDict((prev) => ({...prev, [elementId]: true}));
}
console.log(elements);
};
const value = {
elements,
setElements,
newElement,
elementId,
};
return(
<ElementContext.Provider value={value}>
{children}
</ElementContext.Provider>
)
};
Code Sandbox
Any help is appreciated since I am new and still learning! :)
Few points to handle
const newElement = () => {
newElementId(); // this wont update as react batches the state updates
// try using userRef for elementId
// create new element id here, and then set it
// const newId = elementId + 1
// setElementId(newElementId)
if (!refDict[elementId]) {
setElements(prev => [...prev, { boxtitle: title, boxid: elementId }]);
setRefDict((prev) => ({...prev, [elementId]: true}));
}
console.log(elements);
};
on submit
const object = elements.find(obj => {
if (obj.boxid === boxid){
setBoxHeader(e.target.value) // wont update the as React will likely batch the state updates
return obj
}})
setElements({...object, boxtitle: boxheader}) // is this an array ?
instead try
const object = elements?.find(obj => obj.boxid === boxid)
if (object) {
setBoxHeader(e.target.value)
setElements([ object, { boxtitle: e.target.value, boxId: elementId } ]) // what is the structure of array elements
}
you will find the new React documentation about updating arrays useful
I'm am working on a large React application where performance is critical and unnecessary re-renders are costly.
I have the following example:
const CounterContext = React.createContext();
const CounterProvider = ({children}) => {
const [counterA, setCounterA] = React.useState(0);
const [counterB, setCounterB] = React.useState(0);
return (
<CounterContext.Provider value={{counterA, counterB}}>
{children}
<button onClick={() => setCounterA(counterA + 1)}>Counter A ++</button>
<button onClick={() => setCounterB(counterB + 1)}>Counter B ++</button>
<button onClick={() => {setCounterA(0); setCounterB(0)}}>reset</button>
</CounterContext.Provider>
)
}
const CounterA = () => {
const value = React.useContext(CounterContext);
console.log('CounterA re-render');
return <p>Counter A: {value.counterA}</p>;
}
const CounterB = () => {
const value = React.useContext(CounterContext);
console.log('CounterB re-render');
return <p>Counter B: {value.counterB}</p>;
};
const App = () => {
return (
<CounterProvider>
<CounterA />
<CounterB />
</CounterProvider>
)
};
jsfiddle: https://jsfiddle.net/mitchkman/k3hm0vfq/1/
When clicking the CounterA button, both CounterA and CounterB components will re-render. I'd like to only re-render CounterA, if the counterA property in value changes.
I'd also like to have the ability to have some form of flexibility for conditional re-rendering. This is a pseudocode of what I am trying to do:
const MyComponent = () => {
// Only re-render MyComponent if value.property equals 42
const value = useContext(MyContext, (value) => value.property === 42);
...
};
You may have to wrap the counterProvider to your App component. And use React.memo to achieve re-rendering only on value changes for that component.
I have done something like here in this sandbox
index.js
import { StrictMode } from "react";
import ReactDOM from "react-dom";
import { CounterProvider } from "./AppContext";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<StrictMode>
<CounterProvider>
<App />
</CounterProvider>
</StrictMode>,
rootElement
);
Moved the counterContext to a separate file so that you can import and wrap your App component.
AppContext.js
import React, { useCallback } from "react";
const CounterContext = React.createContext();
export const CounterProvider = ({ children }) => {
const [counterA, setCounterA] = React.useState(0);
const [counterB, setCounterB] = React.useState(0);
const incrementA = () => {
setCounterA(counterA + 1);
};
const incrementB = () => {
setCounterB(counterB + 1);
};
return (
<CounterContext.Provider value={{ counterA, counterB }}>
{children}
<button onClick={incrementA}>Counter A ++</button>
<button onClick={incrementB}>Counter B ++</button>
<button
onClick={useCallback(() => {
setCounterA(0);
setCounterB(0);
}, [])}
>
reset
</button>
</CounterContext.Provider>
);
};
export default CounterContext;
Pass the counterA and counterB values to the Counter components as a props, so that React.memo can check that value before re-rendering the component.
App.js
import React from "react";
import CounterContext from "./AppContext";
const CounterA = React.memo(({ value }) => {
console.log("CounterA re-render");
return <p>Counter A: {value}</p>;
});
const CounterB = React.memo(({ value }) => {
console.log("CounterB re-render");
return <p>Counter B: {value}</p>;
});
const App = () => {
const value = React.useContext(CounterContext);
const counterA = value.counterA;
const counterB = value.counterB;
console.log(counterA);
return (
<>
<CounterA value={counterA} />
<CounterB value={counterB} />
</>
);
};
export default App;
I am trying to call a function from a different component but when I console.log('hi') it appear but it didn't call the messageContext.
Here is my follwing code from Invitees.js:
const [showPreview, setShowPreview] = useState(false);
const toggleUserPreview = () => {
setShowPreview(!showPreview);
};
{showPreview && (
<ResultsWrappers togglePreview={toggleUserPreview}>
<UserPreview
userInfo={applicant}
skillStr={applicant.Skills}
togglePreview={toggleUserPreview}
/>
</ResultsWrappers>
)}
Here is the component have the function I want to call UserPreview.js:
import { useMessageContextProvider } from "../context/MessageContext";
const UserPreview = ({ userInfo, skillStr, togglePreview }) => {
const messageContextProvider = useMessageContextProvider();
const messageUser = () => {
togglePreview();
messageContextProvider.updateActiveUserToMessage(userInfo);
console.log('hi');
};
...
};
Here is my messageContext:
import { createContext, useContext, useState } from "react";
const messageContext = createContext();
export const MessageContextProvider = ({ children }) => {
const [activeUserToMessage, setActiveUserToMessage] = useState({});
const [isOpenMobileChat, toggleMobileChat] = useState(false);
const updateActiveUserToMessage = (user) => {
setActiveUserToMessage(user);
};
return (
<messageContext.Provider
value={{
updateActiveUserToMessage,
activeUserToMessage,
isOpenMobileChat,
toggleMobileChat,
}}
>
{children}
</messageContext.Provider>
);
};
export const useMessageContextProvider = () => {
return useContext(messageContext);
};
When the messageContext called it should open the chatbox like this:
The code you showing is not enough to say it for 100%, but it seems like toggleUserPreview - function called twice, so it reverted to original boolean value.
One time as <ResultsWrappers togglePreview={toggleUserPreview}/>
and second time as <UserPreview togglePreview={toggleUserPreview}/>.
I am checking to see if isFetchingData then don't render yet but its not re-rendering once isFetchingData is set to false. I have the useEffect in the context and i would hope that would re-render once isFetchingData is set to false. Any ideas?
When I refresh the page it renders with the data. So I think its to do with re-rendering.
I am using react context to get the data and exposing functions to filter that data and get me what i need.
Context:
import React, { useEffect, useState } from 'react';
import getAllEmployees from 'my-services/employee/getAllEmployees';
import { arrayOf, node, oneOfType } from 'prop-types';
export const EmployeeContext = React.createContext({
allEmployees: [],
getActiveEmployees: () => [],
getTerminatedEmployees: () => []
});
const EmployeesProvider = ({ children }) => {
const [isFetchingData, setIsFetchingData] = useState(true);
const [allEmployees, setAllEmployees] = useState({});
useEffect(() => {
getAllEmployees().then(
//doing something
).then(employees => {
setAllEmployees(employees);
setIsFetchingData(false);
});
}, [isFetchingData])
const context = {
isFetchingData,
allEmployees,
getActiveEmployees: () =>
allEmployees.filter(x => x.status === 'Current'),
getTerminatedEmployees: () =>
allEmployees.filter(x => x.status === 'Terminated')
};
return (
<EmployeeContext.Provider value={context}>{children}</EmployeeContext.Provider>
);
};
EmployeesProvider.propTypes = {
children: oneOfType([node, arrayOf(node)])
};
EmployeesProvider.defaultProps = {
children: undefined
};
export default EmployeesProvider;
Component:
import React, { useContext } from 'react';
import styled from 'styled-components';
import { EmployeeContext } from 'my-contexts/EmployeeContext';
import EmployeeCard from '../../../components/EmployeeCard';
const EmployeesTab = () => {
const {
getActiveEmployees,
getTerminatedEmployees,
isFetchingData
} = useContext(EmployeeContext);
let activeEmployees = [];
let terminatedEmployees = [];
if (!isFetchingData) {
activeEmployees = getActiveEmployees();
terminatedEmployees = getTerminatedEmployees();
}
if(isFetchingData) {
return <p>Loading</p>;
}
return (
<Outer>
<TopHeader>
<H3>Employees ({activeEmployees.length})</H3>
</TopHeader>
<Wrapper>
{activeEmployees.map(employee => {
return (
<EmployeeCard
id={employee.id}
guid={employee.guid}
firstName={employee.name.first}
lastName={employee.name.last}
jobTitle={employee.jobTitle}
/>
);
})}
</Wrapper>
<H3>Terminated employees({terminatedEmployees.length})</H3>
<Wrapper>
{terminatedEmployees.map(employee => {
return (
<EmployeeCard
id={employee.id}
guid={employee.guid}
firstName={employee.name.first}
lastName={employee.name.last}
jobTitle={employee.jobTitle}
/>
);
})}
</Wrapper>
</Outer>
);
};
export default EmployeesTab;
I think many problems may exist.
At first, please check whether whole component is closed by context Provider.
For example
<EmployeesProvider>
<EmployeesTab/>
<EmployeesProvider/>
Please check this problem.