context is not behaving properly in react. printing default values - javascript

I created below context in react.
NameContext.tsx
import { createContext } from 'react';
export const AppContext = createContext("NA");
I used this context inside my Layout.tsx . I am using inside Layout because I have some business logic to execute. based on that isActive value will be set.
const [isActive,setIsActive]=useState("NO");
return(
<AppContext.Provider value={isActive}>
<User/>
</AppContext.Provider>
)
now under User.tsx I am accessing this tab.
import { useContext } from "react";
import { Link } from "react-router-dom"
import { AppContext } from "../context/NameContext";
const User = ()=>{
const context = useContext(AppContext);
let isActive:any;
if(context=="YES")
isActive=true;
else
isActive=false;
console.log("context is:"+context);
console.log(isActive);
return(
<>
<span>
{isActive && <Link to="user" > List of Users</Link> }
</span>
</>
)
}
export default User;
in just one page refresh it is delivering values many times and most of the time default values. I dont want default values but the value which I set in provider.
how can I avoid this abnormal behaviour.

create a wrapper then put the Layout inside the Wrapper component, the value will be available globally.
//Wrapper
import React, { useState } from "react";
export const Context = React.createContext();
const Wrapper = (props) => {
const [active, setActive] = useState('NO');
function changeValue(value) {
setActive(value)
}
function getCurrent() {
return active;
}
return (
<Context.Provider value={{ active, changeValue, getCurrent }}>
{props.children}
</Context.Provider>
);
};
export default Wrapper;
// index.tsx
import Wrapper from './path/Wrapper'
<Wrapper>
<App />
</Wrapper>
//Layout must be inside App!
//Layout.tsx
import { Context } from "../../layout/Wrapper";
export default function Layout(){
const context = useContext(Context);
console.log(context.getCurrent()) // NO
context.changeValue('YES')
console.log(context.getCurrent()) // YES
}

Related

Using context API (React.js) to change only a particular key in state object

I am using context API in react for managing state. For this I have created a file AppContext.js where I have created context and Provider:
import { useState, createContext } from "react";
export const AppContext = createContext();
export const AppProvider = (props) => {
const [appdata, setAppdata] = useState({
data1: "this is data1",
data2: "this is data2",
});
return (
<AppContext.Provider value={[appdata, setAppdata]}>
{props.children}
</AppContext.Provider>
);
};
I have imported this Provider in the parent component of the app i.e App.js. Also I have wrapped the <AppChild/> component in the Provider.
import AppChild from "./AppChild";
import { AppProvider } from "./AppContext";
const App = () => {
return (
<AppProvider>
<div className="App">hello</div>
<AppChild />
</AppProvider>
);
};
export default App;
Now from AppChild component, I only needed to update the data1 key of my state. For this I have created a button with a onClick through which I will be changing my state. I have used to following code in AppChild.js for this:
import { useContext } from "react";
import { AppContext } from "./AppContext";
const AppChild = () => {
const [appdata, setAppdata] = useContext(AppContext);
return (
<div>
<h3 style={{ color: "red" }}>Data for Appchild: {appdata.data1}</h3>
<button
onClick={() =>
setAppdata((prev) => {
prev.data1 = "updated data1";
return prev;
})
}
>
click to change
</button>
</div>
);
};
export default AppChild;
But when I click the button, the text inside the <h3> block is not changing. Although when I change the whole state by passing the whole object directly to setAppdata as shown below,
This way the state updates successfully. Why is the first method not working where I only wanted to change the data1 key?
You are updating state in wrong way so it is not working. This is how you should update state
<button
onClick={() =>
setAppdata((prevState) => ({
...prevState,
data1: "Your New Data"
}))
}
>
click to change
</button>

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])

Access React context API from _app in NextJS

I need to access the context API in my _app.js file in order to set global state triggered by the router events. The reason for this is to set a loading state which can be accessed by individual components throughout the app. The problem is is the context is provided from the _app.js file, so I don't have the context's context as it were.
context.js
import React, { createContext, useState } from "react";
export const Context = createContext();
const ContextProvider = (props) => {
const [isLoading, setIsLoading] = useState(false);
return (
<Context.Provider
value={{
isLoading,
setIsLoading,
}}
>
{props.children}
</Context.Provider>
);
};
export default ContextProvider;
_app.js
import React, { useContext } from "react";
import App from "next/app";
import Head from "next/head";
import Aux from "../hoc/Aux";
import ContextProvider, { Context } from "../context/context";
import { withRouter } from "next/router";
class MyApp extends App {
static contextType = Context;
componentDidMount() {
this.props.router.events.on("routeChangeStart", () => {
this.context.isLoading(true);
});
this.props.router.events.on("routeChangeComplete", () => {
this.context.isLoading(false);
});
this.props.router.events.on("routeChangeError", () => {
this.context.isLoading(false);
});
}
render() {
const { Component, pageProps } = this.props;
return (
<Aux>
<Head>
<title>My App</title>
</Head>
<ContextProvider>
<Component {...pageProps} />
</ContextProvider>
</Aux>
);
}
}
export default withRouter(MyApp);
Clearly this wouldn't work since _app.js is not wrapped in the context provider. I've tried moving the router event listeners further down the component tree, but then I don't get the loading state from my home page to my dynamically created pages that way.
Is there any workaround that lets me consume context in _app.js? I can't think of any other way I can access loading state globally to conditionally load specific components.
It's not clear to me why you need the context provider to be a parent of _app.js (or a separate component at all). Wouldn't the following work?
class MyApp extends App {
state = {
isLoading: false,
};
componentDidMount() {
this.props.router.events.on("routeChangeStart", () => {
this.setIsLoading(true);
});
this.props.router.events.on("routeChangeComplete", () => {
this.setIsLoading(false);
});
this.props.router.events.on("routeChangeError", () => {
this.setIsLoading(false);
});
}
render() {
const { Component, pageProps } = this.props;
return (
<Aux>
<Head>
<title>My App</title>
</Head>
<Context.Provider
value={{
isLoading: this.state.isLoading,
setIsLoading: this.setIsLoading,
}}>
<Component {...pageProps} />
</Context.Provider>
</Aux>
);
}
setIsLoading = (isLoading) => {
this.setState({ isLoading });
}
}
export default withRouter(MyApp);
Alternatively (if there's something I'm really not understanding about your use case), you could create a HoC:
function withContext(Component) {
return (props) => (
<ContextProvider>
<Component {...props} />
</ContextProvider>
);
}
class MyApp extends App {
...
}
export default withContext(withRouter(MyApp));
You can show a loading indicator using nprogress. For example:
import NProgress from "nprogress";
import Router from "next/router";
Router.onRouteChangeStart = () => NProgress.start();
Router.onRouteChangeComplete = () => NProgress.done();
Router.onRouteChangeError = () => NProgress.done();
Source

React context not updating

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>

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