I want my App component to display keys every time keys changes. I'm doing this by passed keys as a prop of App:
import * as React from "react";
import { render } from "react-dom";
import { useState, useEffect } from "react"
let keys: string[] = [];
// This is what is supposed to happen in the real app
// document.addEventListener("keypress", (event) => {
// keys.push(event.key)
// });
setTimeout(() => {
keys.push('a');
}, 1000)
function App({ keys }: { keys: string[] }) {
let [keysState, setKeysState] = useState(keys)
useEffect(() => {
setKeysState(keys)
}, [keys])
return (
<div>
{keysState.map((key: string) => (
<li>{key}</li>
))}
</div>
);
}
const rootElement = document.getElementById("root");
render(<App keys={keys} />, rootElement);
However, App isn't re-rendering and displaying keys new value:
https://codesandbox.io/s/changing-props-on-react-root-component-forked-3mv0xf?file=/src/index.tsx
Why is this, and how to fix it?
Note: I tried: setKeysState([...keys, 'a']). That doesn't re-render App either.
Live code: https://codesandbox.io/s/changing-props-on-react-root-component-forked-3mv0xf?file=/src/index.tsx
All data that is dynamic needs to be managed by React. Put your event inside the component and update local state.
function App({ initialKeys }: { initialKeys: string[] }) {
const [keys, setKeys] = React.useState(initialKeys);
console.log(keys);
React.useEffect(() => {
const append = (e) => {
setKeys([...keys, e.key]);
};
document.addEventListener("keypress", append);
return () => {
document.removeEventListener("keypress", append);
};
}, [keys]);
return (
<div>
{keys.map((key: string, idx) => (
<li key={idx}>{key}</li>
))}
</div>
);
}
https://codesandbox.io/s/changing-props-on-react-root-component-forked-l869dd?file=/src/index.tsx
if you use the below strategy it works as you want it to work.
React cannot see state changes out of its built-in functions so it didn't track the change on your array which was out of its state scope
import * as React from "react";
import { render } from "react-dom";
import { useState, useEffect } from "react";
let keys: string[] = [];
function App(props: any) {
const [keys, oldKeysState] = useState(props.keys);
const [keysState, setKeysState] = useState(keys);
useEffect(() => {
setKeysState(keys);
}, [keys]);
// componentWillMount
useEffect(() => {
setTimeout(() => {
oldKeysState([...keys, "a"]);
}, 1000);
}, []);
return (
<div>
{keysState.map((key: string) => (
<li>{key}</li>
))}
<button onClick={() => setKeysState([...keysState, "+"])}>+</button>
</div>
);
}
const rootElement = document.getElementById("root");
render(<App keys={keys} />, rootElement);
Related
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;
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 using React's Context API to share data that most of my components need.
The Context is initially defined, but shortly receives data from the Firebase database (please see IdeaContext.tsx). I define the context in a functional component and the display component, which returns a small card based on the information received.
However, the component doesn't render when I start the development server with Yarn. Instead, in order to get it to render, I have to write console.log('something') inside the display component and then it suddenly re-renders. However, when I refresh the server, it again doesn't render.
How can I make my component render immediately (or at least after the context updates with the data from the database?)
Code:
Context Definition:
import React, { createContext, useEffect, useState } from "react";
import { IdeaContextType, Idea } from "../t";
import {ideasRef} from './firebase'
function getIdeas() {
var arr: Array<Idea> = [];
ideasRef.on('value', (snapshot) => {
let items = snapshot.val()
snapshot.forEach( (idea) => {
const obj = idea.val()
arr.push({
title: obj.title,
description: obj.description,
keyID: obj.keyID
})
console.log(arr)
})
})
return arr
}
const IdeaContextDefaultValues: IdeaContextType = {
ideas: [],
setIdeas: () => {},
};
const IdeaContext = createContext<IdeaContextType>(IdeaContextDefaultValues)
const IdeaContextProvider: React.FC = ({ children }) => {
const [ideas, setIdeas] = useState<Array<Idea>>(
IdeaContextDefaultValues.ideas);
useEffect( ()=> {
console.log('getting info')
setIdeas(getIdeas())
}, [])
useEffect( () => {
console.log('idea change: ', ideas)
}, [ideas])
return (
<IdeaContext.Provider value={{ ideas, setIdeas }}>
{children}
</IdeaContext.Provider>
);
};
Displayer and Card Component
import React, { FC, ReactElement, useContext } from "react";
import IdeaCreator from "./IdeaCreator";
import { IdeaContext } from "./IdeaContext";
import { Idea } from "../t";
import { Link } from "react-router-dom";
const IdeaPost:React.FC<Idea> = ({title, keyID, description}):ReactElement => {
console.log('Received',title,description,keyID)
return (
<div className="max-w-sm rounded overflow-hidden shadow-lg">
<img
className="w-full"
src="#"
alt="Oopsy daisy"
/>
<div className="px-6 py-4">
<div className="font-bold text-xl mb-2"> <Link to={"ideas/" + keyID} key= {keyID}> {title}</Link> </div>
<p className="text-gray-700 text-base">{description}</p>
</div>
</div>
);
};
const IdeaDisplay:FC<any> = (props:any):ReactElement => {
const { ideas, setIdeas } = useContext(IdeaContext)
console.log('Ideas in display: ', ideas)
console.log('test') //This is what I comment and uncommend to get it to show
return (
<div className="flex flex-wrap ">
{ideas.map((idea) => {
console.log(idea)
console.log('Sending',idea.title,idea.description,idea.keyID)
console.log(typeof idea.keyID)
return (
<IdeaPost
title={idea.title}
description={idea.description}
keyID = {idea.keyID}
key = {idea.keyID * 100}
/>
);
})}
</div>
);
};
export default IdeaDisplay;
Solution Code:
import React, { createContext, useEffect, useState } from "react";
import { IdeaContextType, Idea } from "../t";
import {ideasRef} from './firebase'
async function getIdeas() {
var arr: Array<Idea> = [];
const snapshot = await ideasRef.once("value");
snapshot.forEach((idea) => {
const obj = idea.val();
arr.push({
title: obj.title,
description: obj.description,
keyID: obj.keyID,
});
console.log(arr);
});
return arr
}
const IdeaContextDefaultValues: IdeaContextType = {
ideas: [],
setIdeas: () => {},
};
const IdeaContext = createContext<IdeaContextType>(IdeaContextDefaultValues)
const IdeaContextProvider: React.FC = ({ children }) => {
const [ideas, setIdeas] = useState<Array<Idea>>(
IdeaContextDefaultValues.ideas);
useEffect(() => {
console.log("getting info");
const setup = async () => {
const ideas = await getIdeas();
setIdeas(ideas);
};
setup()
}, []);
useEffect( () => {
console.log('idea change: ', ideas)
const updateDatabase = async () => {
await ideasRef.update(ideas)
console.log('updated database')
}
updateDatabase()
}, [ideas])
return (
<IdeaContext.Provider value={{ ideas, setIdeas }}>
{children}
</IdeaContext.Provider>
);
};
export {IdeaContext, IdeaContextProvider}
First of all you would need to use once and not on if you want to get the data only once. If you want to use a realtime listener you could send the setIdeas to your function. Also try to be carefull with async/away calls to the Firebase sdk. Your code could look like this:
import React, { createContext, useEffect, useState } from "react";
import { IdeaContextType, Idea } from "../t";
import { ideasRef } from "./firebase";
async function getIdeas() {
var arr: Array<Idea> = [];
const snapshot = await ideasRef.once("value");
let items = snapshot.val();
snapshot.forEach((idea) => {
const obj = idea.val();
arr.push({
title: obj.title,
description: obj.description,
keyID: obj.keyID,
});
console.log(arr);
});
return arr;
}
const IdeaContextDefaultValues: IdeaContextType = {
ideas: [],
setIdeas: () => {},
};
const IdeaContext = createContext < IdeaContextType > IdeaContextDefaultValues;
const IdeaContextProvider: React.FC = ({ children }) => {
const [ideas, setIdeas] =
useState < Array < Idea >> IdeaContextDefaultValues.ideas;
useEffect(() => {
console.log("getting info");
const getData = async () => {
const ideas = await getIdeas();
setIdeas(ideas);
};
}, []);
useEffect(() => {
console.log("idea change: ", ideas);
}, [ideas]);
return (
<IdeaContext.Provider value={{ ideas, setIdeas }}>
{children}
</IdeaContext.Provider>
);
};
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.
See codesandbox here
I am trying to pass the state value of a child component up to its parent by using React's useImperativeHandle. However, it appears that my parent component is not receiving the updated state value of the child component when it console logs the child's component value; console.log(componentRef.current.state) always is logged as false.
Why is this not working and how can I accurately receive the mutated state value of my child component in my parent component by passing the necessary ref? Thanks!
index.tsx:
import React from "react";
import ReactDOM from "react-dom";
const Component = React.forwardRef((props, ref) => {
const [state, set] = React.useState(false);
React.useImperativeHandle(ref, () => ({
state
}));
const handleClick = () => {
set(prevState => !prevState);
};
return (
<>
<button type="button" onClick={handleClick}>
Click Me
</button>
<h1>{state ? "On" : "Off"}</h1>
</>
);
});
const App = () => {
const componentRef = React.useRef(null);
React.useEffect(() => {
console.log(componentRef.current.state);
}, [componentRef]);
return <Component ref={componentRef} />;
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
If you just want that functionality you can use something like:
import React, { useEffect } from "react";
import ReactDOM from "react-dom";
const Component = props => {
const [state, set] = React.useState(false);
useEffect(() => props.callback(state), [state])
const handleClick = () => {
set(prevState => !prevState);
};
return (
<>
<button type="button" onClick={handleClick}>
Click Me
</button>
<h1 >{state ? "On" : "Off"}</h1>
</>
);
};
const App = () => {
return <Component callback={val => console.log(val)} />;
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
But if you really need to use ref, just comment it so I'll remove this answer.