React context not updating - javascript

I have set a basic sample project that use Context to store the page title, but when I set it the component is not rerendered.
Principal files:
Context.js
import React from 'react'
const Context = React.createContext({})
export default Context
AppWrapper.js
import React from 'react'
import App from './App'
import Context from './Context'
function AppWrapper () {
return (
<Context.Provider value={{page: {}}}>
<App />
</Context.Provider>
)
}
export default AppWrapper
App.js
import React, { useContext } from 'react';
import Context from './Context';
import Home from './Home';
function App() {
const { page } = useContext(Context)
return (
<>
<h1>Title: {page.title}</h1>
<Home />
</>
);
}
export default App;
Home.js
import React, { useContext } from 'react'
import Context from './Context'
function Home () {
const { page } = useContext(Context)
page.title = 'Home'
return (
<p>Hello, World!</p>
)
}
export default Home
full code
What am I doing wrong?

Think about React context just like you would a component, if you want to update a value and show it then you need to use state. In this case your AppWrapper where you render the context provider is where you need to track state.
import React, {useContext, useState, useCallback, useEffect} from 'react'
const PageContext = React.createContext({})
function Home() {
const {setPageContext, page} = useContext(PageContext)
// essentially a componentDidMount
useEffect(() => {
if (page.title !== 'Home')
setPageContext({title: 'Home'})
}, [setPageContext])
return <p>Hello, World!</p>
}
function App() {
const {page} = useContext(PageContext)
return (
<>
<h1>Title: {page.title}</h1>
<Home />
</>
)
}
function AppWrapper() {
const [state, setState] = useState({page: {}})
const setPageContext = useCallback(
newState => {
setState({page: {...state.page, ...newState}})
},
[state, setState],
)
const getContextValue = useCallback(
() => ({setPageContext, ...state}),
[state, updateState],
)
return (
<PageContext.Provider value={getContextValue()}>
<App />
</PageContext.Provider>
)
}
Edit - Updated working solution from linked repository
I renamed a few things to be a bit more specific, I wouldn't recommend passing setState through the context as that can be confusing and conflicting with a local state in a component. Also i'm omitting chunks of code that aren't necessary to the answer, just the parts I changed
src/AppContext.js
export const updatePageContext = (values = {}) => ({ page: values })
export const updateProductsContext = (values = {}) => ({ products: values })
export const Pages = {
help: 'Help',
home: 'Home',
productsList: 'Products list',
shoppingCart: 'Cart',
}
const AppContext = React.createContext({})
export default AppContext
src/AppWrapper.js
const getDefaultState = () => {
// TODO rehydrate from persistent storage (localStorage.getItem(myLastSavedStateKey)) ?
return {
page: { title: 'Home' },
products: {},
}
}
function AppWrapper() {
const [state, setState] = useState(getDefaultState())
// here we only re-create setContext when its dependencies change ([state, setState])
const setContext = useCallback(
updates => {
setState({ ...state, ...updates })
},
[state, setState],
)
// here context value is just returning an object, but only re-creating the object when its dependencies change ([state, setContext])
const getContextValue = useCallback(
() => ({
...state,
setContext,
}),
[state, setContext],
)
return (
<Context.Provider value={getContextValue()}>
...
src/App.js
...
import AppContext, { updateProductsContext } from './AppContext'
function App() {
const [openDrawer, setOpenDrawer] = useState(false)
const classes = useStyles()
const {
page: { title },
setContext,
} = useContext(Context)
useEffect(() => {
fetch(...)
.then(...)
.then(items => {
setContext(updateProductsContext({ items }))
})
}, [])
src/components/DocumentMeta.js
this is a new component that you can use to update your page names in a declarative style reducing the code complexity/redundancy in each view
import React, { useContext, useEffect } from 'react'
import Context, { updatePageContext } from '../Context'
export default function DocumentMeta({ title }) {
const { page, setContext } = useContext(Context)
useEffect(() => {
if (page.title !== title) {
// TODO use this todo as a marker to also update the actual document title so the browser tab name changes to reflect the current view
setContext(updatePageContext({ title }))
}
}, [title, page, setContext])
return null
}
aka usage would be something like <DocumentMeta title="Whatever Title I Want Here" />
src/pages/Home.js
each view now just needs to import DocumentMeta and the Pages "enum" to update the title, instead of pulling the context in and manually doing it each time.
import { Pages } from '../Context'
import DocumentMeta from '../components/DocumentMeta'
function Home() {
return (
<>
<DocumentMeta title={Pages.home} />
<h1>WIP</h1>
</>
)
}
Note: The other pages need to replicate what the home page is doing
Remember this isn't how I would do this in a production environment, I'd write up a more generic helper to write data to your cache that can do more things in terms of performance, deep merging.. etc. But this should be a good starting point.

Here is a working version of what you need.
import React, { useState, useContext, useEffect } from "react";
import "./styles.css";
const Context = React.createContext({});
export default function AppWrapper() {
// creating a local state
const [state, setState] = useState({ page: {} });
return (
<Context.Provider value={{ state, setState }}> {/* passing state to in provider */}
<App />
</Context.Provider>
);
}
function App() {
// getting the state from Context
const { state } = useContext(Context);
return (
<>
<h1>Title: {state.page.title}</h1>
<Home />
</>
);
}
function Home() {
// getting setter function from Context
const { setState } = useContext(Context);
useEffect(() => {
setState({ page: { title: "Home" } });
}, [setState]);
return <p>Hello, World!</p>;
}
Read more on Hooks API Reference.

You may put useContext(yourContext) at wrong place.
The right position is inner the <Context.Provider>:
// Right: context value will update
<Context.Provider>
<yourComponentNeedContext />
</Context.Provider>
// Bad: context value will NOT update
<yourComponentNeedContext />
<Context.Provider>
</Context.Provider>

Related

How to write cutsom redux hooks replicating what redux does fundamentally?

Just stumbled upon the question of writing custom redux hooks.
Here is a backbone of the App.js
import { Provider, useStore, useDispatch } from "./redux";
import reducers from "./reducers";
import "./styles.css";
import React} from "react";
function Hello() {
const counter = useStore(store => store.counter);
const dispatch = useDispatch();
const increment = () => dispatch({ type: "INCREMENT" });
const decrement = () => dispatch({ type: "DECREMENT" });
return (
<div>
<h3>Counter: 0</h3>
<button onClick={increment}>+ Increment</button>
<button onClick={decrement}>- Decrement</button>
</div>
);
}
// <Provider reducers={reducers}>
export default function App() {
return (
<div className="App">
<Provider reducers={reducers}>
<Hello />
</Provider>
</div>
);
}
The idea is to write implementations for useStore, useDispatch and Provider.
I got the idea that we should use context api and useReducer to have access to dispatch but then I got stuck.
I know Provider can something be like
const initialState = {};
const context = createContext(initialState);
export function Provider({ children, reducers }) {
const [state, dispatch] = useReducer(reducers, initialState);
// children need to have access to dispatch
return (
<context.Provider value={{ state, dispatch }}>{children}</context.Provider>
);
}
And similarly we could have used useContext to have access to the value passed
like
export function useStore(selector) {
const { state} = useContext(context);
return { state.selector};
}
/**
* Returns the dispatch function
*/
export function useDispatch() {
const { dispatch } = useContext(context);
return { dispatch };
}
reducers file is also a part of the sandbox link given below.
But I am stuck and cannot figure why it's not working.
Please help and explain.
Here is the work in progress sandbox
https://codesandbox.io/s/redux-forked-tlb4hb?file=/src/redux.js

React elements are removed on state update

I'm supposed to have a modal appear with an image in it. There are next and previous buttons which controls which image you are currently viewing. The modal is rendered in a portal. That in itself is working correctly. However, when I add children, and those childrens are updated, the modal only (not the portal) gets removed from the flow. In the React DevTools, the "isOpen" state of the modal is still set to true. I am using React 17.0.2 with NextJS 12.0.4 and Styled Components 5.3.3.
I have tried:
memoizing my components (as you can see there are some remnants of those trials) but this did not work
extracting the state of the modal to the parent and passing it as props and it didn't work either
I know there must be something wrong that I'm doing here so if you could help me find it that would be much appreciated!
Here is the controller where the modal is rendered:
import { FC, MouseEventHandler, useEffect, useState } from "react";
import { Photo } from "services/Images/Images.interfaces";
import { useGetNextPhoto, useGetPhotos, useGetPreviousPhoto } from "state";
import SlideshowContextProvider from "./Context/SlideshowContext";
import SlideShowModal from "./SlideShowModal";
const SlideshowController: FC = () => {
const photos = useGetPhotos();
const [currentlyViewedPhoto, setCurrentlyViewedPhoto] = useState<Photo | null>(null);
const nextPhoto = useGetNextPhoto(currentlyViewedPhoto?.id);
const previousPhoto = useGetPreviousPhoto(currentlyViewedPhoto?.id);
const onPreviousRequest: MouseEventHandler<HTMLButtonElement> = (event) => {
event.preventDefault();
setCurrentlyViewedPhoto(previousPhoto);
};
const onNextRequest: MouseEventHandler<HTMLButtonElement> = async (event) => {
event.preventDefault();
setCurrentlyViewedPhoto(nextPhoto);
};
useEffect(() => {
setCurrentlyViewedPhoto(photos[0]);
}, [photos]);
return (
<SlideshowContextProvider
currentlyViewing={currentlyViewedPhoto}
onNextSlideRequest={onNextRequest}
onPreviousSlideRequest={onPreviousRequest}
>
<SlideShowModal />
</SlideshowContextProvider>
);
};
export default SlideshowController;
The SlideshowModal:
import { Modal } from "components";
import { FC } from "react";
import SlideshowControlBar from "./SlideshowControlBar";
import SlideshowImage from "./SlideshowImage";
const SlideShowModal: FC = () => {
return (
<Modal uniqueKey="slideshow">
<SlideshowImage />
<SlideshowControlBar />
</Modal>
);
};
export default SlideShowModal;
The modal in itself:
import Portal from "components/Portal/Portal";
import { FC, useEffect, useMemo, useState } from "react";
import { useRegisterModal } from "state";
import styled from "styled-components";
import useWindowScrollLock from "./hook/UseWindowScrollLock";
interface Props {
uniqueKey: string;
isBackgroundOpaque?: boolean;
}
... Styled elements
const Modal: FC<Props> = ({ uniqueKey, isBackgroundOpaque = true, children }) => {
const [isOpen, setIsOpen] = useState(false);
const open = () => setIsOpen(true);
const close = () => setIsOpen(false);
const register = useRegisterModal(uniqueKey);
const isModalOpen = useMemo(() => isOpen, [isOpen]);
useEffect(() => {
register({ open, close });
}, [register]);
useWindowScrollLock(isModalOpen);
return isModalOpen ? (
<Portal>
<Container>
<InnerModal>
<Close onClick={close}>X</Close>
{children}
</InnerModal>
</Container>
<Background onClick={close} opaque={isBackgroundOpaque} />
</Portal>
) : null;
};
export default Modal;

How do I get item from localstorage?

I try to get an item from my localstorage with next.js without rerender my page.
I use this code:
import { ThemeProvider } from "#material-ui/core";
import { FC, useEffect, useState } from "react";
import { useStore } from "../hooks/ContextStore";
import { darkTheme, theme } from "../theme/theme";
import Navbar from "./navbar/Navbar";
const Layout: FC = ({ children }) => {
const [darkMode, setDarkMode] = useState(false);
const store = useStore();
useEffect(() => {
setDarkMode(JSON.parse(window.localStorage.getItem("dark-mode")));
}, [store.darkMode]);
console.log("did render");
return (
<ThemeProvider theme={darkMode ? darkTheme : theme}>
<Navbar />
{children}
</ThemeProvider>
);
};
export default Layout;
Because my useEffect update my state it is rendering the page twice when I turn on or off the dark mode state.
Is there a way to prevent it from rerender that much and accessing the localstorage?
From the looks of it, because your state value is alway going to be from the localstorage, then you don't need to store it in the state.
You can just do something like so:
const isClient = typeof window !== undefined;
const Layout: FC = ({ children }) => {
const store = useStore();
const darkMode = isClient && JSON.parse(window.localStorage.getItem("dark-mode"));
return (
<ThemeProvider theme={darkMode ? darkTheme : theme}>
<Navbar />
{children}
</ThemeProvider>
);
}
export default Layout;
If you really want to store in the state, then you should update the dependency array in useEffect to look for updates in the localstorage value, not the state because you'll be updating the state.
const storageDarkMode = JSON.parse(window.localStorage.getItem("dark-mode"));
useEffect(() => {
setState(storageDarkMode);
}, [storageDarkMode])

React useContext and useMemo for Global variables

Would like to seek guidance from folks if this React implementation makes sense and understand the pitfalls if any. The implementation works but I am unsure if its the correct practice. Please kindly advise.
Idea - Create an AppContext that allows reusability of global states (or even functions) - instead of the conventional useContext + useReducer
AppContext.jsx
import React from 'react';
export const AppContext = React.createContext(null);
export const AppContextProvider = (props) => {
const [ appState, setAppState ] = React.useState({});
const appStateProvider = React.useMemo(() => ({ appState, setAppState }), [ appState, setAppState ]);
const setAppStateItem = (key, value) => {
appStateProvider.setAppState(state => { return { ...state, [key]: value} })
return value;
}
const getAppStateItem = (key = '', fallback) => {
return appState[key] || fallback;
}
const deleteAppStateItem = (key = '') => {
if(key in appState) {
appStateProvider.setAppState(state => {
state[key] = undefined;
return state;
})
}
}
return (
<AppContext.Provider value={{ appStateProvider, setAppStateItem, getAppStateItem, deleteAppStateItem }}>
{props.children}
</AppContext.Provider>
)
}
Create.jsx
import React from 'react';
import { AppContext } from 'contexts';
const { setAppStateItem } = React.useContext(AppContext);
....
setAppStateItem('count', 5);
....
Consume.jsx
import React from 'react';
import { AppContext } from 'contexts';
const { getAppStateItem, setAppStateItem } = React.useContext(AppContext);
....
const count = getAppStateItem('count');
....
Here was an approach to create a global state using useContext and useReducer following a pattern similar to redux. You essentially set up a Store with useReducer and a Context.Provider that you then wrap the rest of your application in. Here was a small implementation I had going:
import React, { createContext, useReducer } from "react";
import Reducer from './Reducer'
const initialState = {
openClose: false,
openOpen: false,
ticker: "BTCUSDT",
tickerRows: [],
positionRows: [],
prices: {},
walletRows: []
};
const Store = ({ children }) => {
const [state, dispatch] = useReducer(Reducer, initialState);
return (
<Context.Provider value={[state, dispatch]}>
{children}
</Context.Provider>
)
};
export const ACTIONS = {
SET_CLOSE_OPEN: 'SET_CLOSE_OPEN',
SET_OPEN_OPEN: 'SET_OPEN_OPEN',
SET_TICKER: 'SET_TICKER',
SET_TICKER_ROWS: 'SET_TICKER_ROWS',
SET_POSITION_ROWS: 'SET_POSITION_ROWS',
SET_PRICES: 'SET_PRICES',
SET_WALLET_ROWS: 'SET_WALLET_ROWS'
}
export const Context = createContext(initialState);
export default Store;
Here is the reducer:
import { ACTIONS } from './Store'
const Reducer = (state, action) => {
switch (action.type) {
case ACTIONS.SET_CLOSE_OPEN:
return {
...state,
openClose: action.payload
};
case ACTIONS.SET_OPEN_OPEN:
return {
...state,
openOpen: action.payload
};
...
default:
return state;
}
};
export default Reducer;
I put the Store component in index.js so that it's context is available to all of the components in the app.
ReactDOM.render(
<React.StrictMode>
<Store>
<App />
</Store>
</React.StrictMode>,
document.getElementById('root')
);
Then when you want to access and update the state, you just import the actions and context and the useContext hook:
import { useContext} from "react";
import { ACTIONS, Context } from './store/Store';
and then you just add
const [state, dispatch] = useContext(Context);
inside your functional component and you can access the state and use dispatch to update it.
One limitation is that every component that accesses the state with useContext re-renders every time anything in the state gets updated, not just the part of the state that the component depends on. One way around this is to use the useMemo hook to control when the component re-renders.
export default function WalletTable({ priceDecimals }) {
const classes = useStyles();
const [state, dispatch] = useContext(Context);
async function getCurrentRows() {
const response = await fetch("http://localhost:8000/wallet");
const data = await response.json();
dispatch({ type: ACTIONS.SET_WALLET_ROWS, payload: data });
}
useEffect(() => {
getCurrentRows();
}, []);
const MemoizedWalletTable = useMemo(() => {
return (
<TableContainer component={Paper}>
...
</TableContainer>
);
}, [state.walletRows])
return MemoizedWalletTable
}
Having to memoize everything makes it seem like maybe just using redux isn't all that much more complicated and is easier to deal with once set up.

React: Context to pass state between two hierarchies of components

I am developing a website in which I want to be able to access the state information anywhere in the app. I have tried several ways of implementing state but I always get following error message:
Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
Check the render method of SOS.
Here is my SOS->index.js file:
import React, { useContext } from 'react';
import axios from 'axios';
import CONST from '../utils/Constants';
import { Grid, Box, Container } from '#material-ui/core';
import { styled } from '#material-ui/styles';
import { Header } from '../Layout';
import ListItem from './ListItem';
import SOSButton from './SOSButton';
import FormPersonType from './FormPersonType';
import FormEmergencyType from './FormEmergencyType';
import StateContext from '../App';
import Context from '../Context';
export default function SOS() {
const { componentType, setComponentType } = useContext(Context);
const timerOn = false;
//'type_of_person',
const ambulance = false;
const fire_service = false;
const police = false;
const car_service = false;
//static contextType = StateContext;
const showSettings = event => {
event.preventDefault();
};
const handleComponentType = e => {
console.log(e);
//this.setState({ componentType: 'type_of_emergency' });
setComponentType('type_of_emergency');
};
const handleEmergencyType = new_emergency_state => {
console.log(new_emergency_state);
// this.setState(new_emergency_state);
};
const onSubmit = e => {
console.log('in OnSubmit');
axios
.post(CONST.URL + 'emergency/create', {
id: 1,
data: this.state //TODO
})
.then(res => {
console.log(res);
console.log(res.data);
})
.catch(err => {
console.log(err);
});
};
let component;
if (componentType == 'type_of_person') {
component = (
<FormPersonType handleComponentType={this.handleComponentType} />
);
} else if (componentType == 'type_of_emergency') {
component = (
<FormEmergencyType
handleComponentType={this.handleComponentType}
handleEmergencyType={this.handleEmergencyType}
emergencyTypes={this.state}
timerStart={this.timerStart}
onSubmit={this.onSubmit}
/>
);
}
return (
<React.Fragment>
<Header title="Send out SOS" />
<StateContext.Provider value="type_of_person" />
<Container component="main" maxWidth="sm">
{component}
</Container>
{/*component = (
<HorizontalNonLinearStepWithError
handleComponentType={this.handleComponentType}
/>*/}
</React.Fragment>
);
}
I would really appreciate your help!
Just for reference, the Context file is defined as follows:
import React, { useState } from 'react';
export const Context = React.createContext();
const ContextProvider = props => {
const [componentType, setComponentType] = useState('');
setComponentType = 'type_of_person';
//const [storedNumber, setStoredNumber] = useState('');
//const [functionType, setFunctionType] = useState('');
return (
<Context.Provider
value={{
componentType,
setComponentType
}}
>
{props.children}
</Context.Provider>
);
};
export default ContextProvider;
EDIT: I have changed my code according to your suggestions (updated above). But now I get following error:
TypeError: Cannot read property 'componentType' of undefined
Context is not the default export from your ../Context file so you have to import it as:
import { Context } from '../Context';
Otherwise, it's trying to import your Context.Provider component.
For your file structure/naming, the proper usage is:
// Main app file (for example)
// Wraps your application in the context provider so you can access it anywhere in MyApp
import ContextProvider from '../Context'
export default () => {
return (
<ContextProvider>
<MyApp />
</ContextProvider>
)
}
// File where you want to use the context
import React, { useContext } from 'react'
import { Context } from '../Context'
export default () => {
const myCtx = useContext(Context)
return (
<div>
Got this value - { myCtx.someValue } - from context
</div>
)
}
And for godsakes...rename your Context file, provider, and everything in there to something more explicit. I got confused even writing this.

Categories

Resources