Using the context API in React - javascript

I'm starting to work with the context api in React, and I decided to make a simple theme context. For now, if I go to my React DevTools and change the boolean isLightTheme to false, it works, but when i try to attach this functionality to a button, i keep getting errors. Can saomeone help me with that?
theme context
import React, { useState, createContext } from 'react'
export const ThemeContext = createContext()
export const ThemeContextProvider = ({ children }) => {
const [state, setState] = useState({
isLightTheme: true,
light: { syntax: '#555', ui: '#ddd', bg: '#eee' },
dark: { syntax: '#ddd', ui: '#333', bg: '#555' }
})
const toggleTheme = () => {
setState({ isLightTheme: !state.isLightTheme})
}
return (
<ThemeContext.Provider value={{...state, toggleTheme}}>
{children}
</ThemeContext.Provider>
)
}
import React, { useContext } from 'react'
import { ThemeContext } from '../context/ThemeContext'
export const ThemeToggler = () => {
const themeContext = useContext(ThemeContext)
const { toggleTheme } = themeContext
return (
<button onClick={toggleTheme}>Change Theme</button>
)
}

The useState hooks API doesn't update a selective piece of state like setState does. It'll override the entire state. Here when you toggle the theme, the new state is only
{ isLightTheme: !state.isLightTheme} with no light and dark keys. You just need to handle this by only updating that piece of state. This should work :
import React, { useState, createContext } from 'react'
export const ThemeContext = createContext()
export const ThemeContextProvider = ({ children }) => {
const [state, setState] = useState({
isLightTheme: true,
light: { syntax: '#555', ui: '#ddd', bg: '#eee' },
dark: { syntax: '#ddd', ui: '#333', bg: '#555' }
})
const toggleTheme = () => {
//preserve the remaining state also
setState({...state,isLightTheme: !state.isLightTheme})
}
return (
<ThemeContext.Provider value={{...state, toggleTheme}}>
{children}
</ThemeContext.Provider>
)
}
Hope this helps !

Related

ReactJS giving error Uncaught TypeError: __WEBPACK_IMPORTED_MODULE_0__... is undefined when implementing Context API

I'm using Context API for a cryptocurrency tracker app. What I want is to create the state currency and setCurrency from the context named CryptoState which I created in CryptoContext.js and import to Header component so I can use them to do the function onChange. When I import state from CryptoState it has the error as in the title and causes the page emptied.
Here's my code:
CryptoContext.js
import { createContext, useContext, useState, useEffect } from 'react'
const Crypto = createContext()
const CryptoContext = ({ children }) => {
const [currency, setCurrency] = useState("VND")
const [symbol, setSymbol] = useState("₫")
useEffect(() => {
if (currency === "VND") setSymbol("₫")
else if (currency === "USD") setSymbol("$")
}, [currency])
return (
<Crypto.Provider value={{currency, symbol, setCurrency}}>{children}</Crypto.Provider>
)
}
export default CryptoContext
export const CryptoState = () => {
return useContext(Crypto)
}
Header.js
import {
AppBar,
Container,
Toolbar,
Typography,
Select,
MenuItem,
} from '#material-ui/core'
import { makeStyles, createTheme, ThemeProvider } from '#material-ui/core/styles'
import { Link } from 'react-router-dom'
import { CryptoState } from '../CryptoContext'
const useStyles = makeStyles(() => ({
title: {
flex: 1,
color: 'gold',
fontFamily: 'Montserrat',
fontWeight: 'bold',
cursor: 'pointer'
}
}))
const Header = () => {
const classes = useStyles()
const { currency, setCurrency } = CryptoState()
const darkTheme = createTheme({
palette: {
primary: {
main: '#fff',
},
type: 'dark'
}
})
return (
<ThemeProvider theme={darkTheme}>
<AppBar color="transparent" position="static">
<Container>
<Toolbar>
<Typography
className={classes.title}
variant='h6'
>
<Link to="/">
Crypto Hunter
</Link>
</Typography>
<Select
variant="outlined"
style={{
width: 100,
height: 40,
marginRight: 15,
}}
defaultValue={'VND'}
value={currency}
onChange={(e) => setCurrency(e.target.value)}
>
<MenuItem value={"USD"}>USD</MenuItem>
<MenuItem value={"VND"}>VND</MenuItem>
</Select>
</Toolbar>
</Container>
</AppBar>
</ThemeProvider>
)
}
export default Header
As soon as I import the state by this line: const { currency, setCurrency } = CryptoState() in Header.js, it causes error TypeError: _CryptoContext__WEBPACK_IMPORTED_MODULE_0__.CryptoState() is undefined and cause the page emptied.
Here's the captured image of the error: error's image
All answers are highly appreciated. Thank you 🙏
You are wrong now.
Try to rename CryptoContext to CryptoProvider.
Where does Header is render? in App.js?
Please find out where is contain and wrap the Provider of CryptoProvider:
<CryptoProvider>
<Header />
</CryptoProvider>
In Header.js component. You have to use the variable you set for createContext function. then, please use useContext instead of calling it directly
// CryptoContext.js
const cryptoContext = createContext(); // remember to export this var.
Then, in Header.js
import { useContext } from 'react';
import { cryptoContext } from './CryptoContext';
const { currency, setCurrency } = useContext(cryptoContext);
Hey Guys so i was having this error below
The main issue is just to make sure your naming convention for exporting your useContext must be in camelCase Naming convention and make sure your index.js is wrapped around with your Context provider.
so instead of this
export const UseStateContext = () => {
return useContext(StateContext);
};
DO THIS!!!
export const useStateContext = () => {
return useContext(StateContext);
};

how to change value of react Context from another component React

so i'm trying to implement a simple theme switch via react context, and i need to change the value of context(in ThemeProvider.jsx) provider according to a onChange event in another component(ThemeSwitcher.jsx).
ThemeProvider.jsx :
import React, {createContext} from "react";
import {THEME_TYPE} from "../constants";
export const ThemeContext = createContext(THEME_TYPE.LIGHT);
const ThemeProvider = ({ children }) => {
return <>
<ThemeContext.Provider value={//THEME_TYPE.LIGHT or THEME_TYPE.DARK)}>
{children}
</ThemeContext.Provider>
</>
};
export default ThemeProvider;
ThemeSwitcher.jsx :
import React, {useContext} from "react";
import { THEME_TYPE } from "../constants";
import {ThemeContext} from "../providers/ThemeProvider";
const ThemeSwitcher = () => {
const themeMode = useContext(ThemeContext);
const handleThemeChange = (e) => {
//value of context should change according to argument 'e'
};
return (
<div className="switch-container">
<label className="switch">
<input
data-testid="theme-changer"
type="checkbox"
checked={themeMode === THEME_TYPE.DARK}
onChange={handleThemeChange}
/>
<span className="slider round"></span>
</label>
<span className="text-color bold">Dark mode</span>
</div>
);
};
export default ThemeSwitcher;
App.jsx:
import React, {useContext} from "react";
import { Helmet } from "react-helmet";
import NameBox from "./components/NameBox";
import ThemeSwitcher from "./components/ThemeSwitcher";
import { THEME_TYPE } from "./constants";
import Styles from "./data/Styles";
import ThemeProvider from "./providers/ThemeProvider";
import {ThemeContext} from "./providers/ThemeProvider";
const StyleTag = () => {
const themeMode = useContext(ThemeContext);
return (
<Helmet>
<style>{Styles(themeMode)}</style>
</Helmet>
);
};
function App() {
return (
<ThemeProvider>
<StyleTag />
<NameBox />
<ThemeSwitcher />
</ThemeProvider>
);
}
export default App;
and styles.js if necessary:
import { THEME_TYPE } from "../constants";
const Theme = {
[THEME_TYPE.LIGHT]: {
background: "#fafafa",
text: "#rgba(0, 0, 0, 0.87)",
divider: "rgba(0, 0, 0, 0.12)",
},
[THEME_TYPE.DARK]: {
background: "#303030",
text: "#fff",
divider: "rgba(255, 255, 255, 0.12)",
},
};
const Styles = (theme) => `
body {background-color: ${Theme[theme].background};}
.text-color {color: ${Theme[theme].text};}
.box {border: 1px solid ${Theme[theme].divider}}
`;
export default Styles;
as you see the value of context should be changed according to input onChange event. i couldn't come up with proper solution for relating these two, so your help is appreciated.
I suggest adding useState() hook inside ThemeProvider component.
Here is the codesandbox: https://codesandbox.io/s/magical-franklin-cril0?file=/src/ThemeProvider.jsx
That is how the code looks like:
import React, { createContext, useState } from "react";
import { THEME_TYPE } from "./constants";
export const ThemeContext = createContext(THEME_TYPE.LIGHT);
const ThemeProvider = ({ children }) => {
const [themeType, setThemeType] = useState(THEME_TYPE.LIGHT);
const changeTheme = (value) => {
if (value) {
setThemeType(THEME_TYPE.DARK);
} else {
setThemeType(THEME_TYPE.LIGHT);
}
};
return (
<>
<ThemeContext.Provider value={{ themeType, changeTheme }}>
{children}
</ThemeContext.Provider>
</>
);
};
export default ThemeProvider;
And then you would use the context where needed like this:
const themeMode = useContext(ThemeContext)
themeMode.themeType // THEME_TYPE.LIGHT or THEME_TYPE.DARK
themeMode.changeTheme(value) // if value is true, it would change to dark mode, if false to light mode

Call function on application load in React Native

I have a React Native application and it has tabbed layout. I need to call some function when the main screen is loaded. I tried to use componentWillMount() function, but it didn't work because my screen was defined in function and not in class. How can I create on load function?
HomeScreen.js
import React, { useState, Component } from 'react';
import { View, Text } from 'react-native';
import { getText } from '..components/getText';
export default function HomeScreen() {
const [onLoadText, setText] = useState("");
const onScreenLoad = () => {
setText(getText());
}
const componentWillMount = () => {
// Not working
onScreenLoad();
}
return (
<View style={styles.container}>
<Text>{onLoadText}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
});
since you're using a stateless component, you can use the useEffect hook
useEffect(() => {
// write your code here, it's like componentWillMount
}, [])
You must add useEffect
import React, { useState, Component, useEffect } from 'react';
import { View, Text } from 'react-native';
import { getText } from '..components/getText';
export default function HomeScreen() {
const [onLoadText, setText] = useState("");
const onScreenLoad = () => {
setText(getText());
}
useEffect(() => {
// write your code here, it's like componentWillMount
onScreenLoad();
}, [])
return (
<View style={styles.container}>
<Text>{onLoadText}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
});

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 jest pass data to mounted component

I am trying to test a component that use context. After I mount it (shallow does not work with useContext apparently) I am trying to set default values for the component data.
I was expecting const contextValues = { text: 'mock', msg: 'SUCCESS' }; and passing that to the AlertContextProvider to set a state for that component but I am probably looking at this the wrong way.
AlertContext.js:
import React, { createContext, useState, useContext } from 'react';
export const AlertContext = createContext();
const AlertContextProvider = props => {
const [alert, setAlert] = useState({
text: '',
msg: ''
});
const updateAlert = (text, msg) => {
setAlert({
text,
msg
});
};
return (
<AlertContext.Provider value={{ alert, updateAlert }}>
{props.children}
</AlertContext.Provider>
);
};
export default AlertContextProvider;
Alert.js (component):
import React, { useContext } from 'react';
import './Alert.scss';
import { AlertContext } from '../context/AlertContext';
const Alert = () => {
const { alert } = useContext(AlertContext);
return (
<div className='alert'>
<p className="alert-para">{alert.text}</p>
</div>
);
};
export default Alert;
Alert.js(text)
import React from 'react';
import { mount } from 'enzyme';
import Alert from '../components/Alert';
import AlertContextProvider from '../context/AlertContext';
describe('Alert', () => {
let wrapper;
beforeEach(() => {
const contextValues = { text: 'mock', msg: 'SUCCESS' };
// Below mounting is needed as Enzyme does not yet support shallow mocks
wrapper = mount(
<AlertContextProvider value={contextValues}>
<Alert />
</AlertContextProvider>
);
});
test('Should render a paragraph', () => {
const element =wrapper.find('.alert-para');
expect(element.length).toBe(1); // this is correct
expect(element.text()).toEqual('mock'); // THIS FAILS AS THE VALUE OF THE ELEMENT IS AN EMPTY STRING WHILE I WAS EXPECTING 'mock'
});
});
You are passing your contextValues through value prop on <AlertContextProvider /> but you are never using that prop to initialize data inside your context provider.
In this example, I used useEffect hook as componentDidMount to initialize your state AlertContext.js`
const AlertContextProvider = props => {
const [alert, setAlert] = useState({
text: '',
msg: ''
});
// The same as component did mount
useEffect(() => {
setAlert({
text: props.value.text,
msg: props.value.msg
})
}, [])
const updateAlert = (text, msg) => {
setAlert({
text,
msg
});
};
return (
<AlertContext.Provider value={{ alert, updateAlert }}>
{props.children}
</AlertContext.Provider>
);
};
You should use useCallback hook for your updateAlert function to memoize it.

Categories

Resources