'dispatch' is not defined when using useReducer with useContext in react - javascript

I'm trying to figure out how to change global state from a componenet using useContext and useReducer dispatch method.
The component is simply should change the backgournd of the page on a click
Here is how I defined the context ThemeContext.js
import { createContext, useReducer } from "react";
import ThemeReducer from './ThemeReducer'
const INITIAL_STATE = {
isLightTheme: true,
light: {syntax: '#555', ui: '#ddd', bg: '#eee'},
dark: {syntax: '#ddd', ui: '#333', bg: '#555'},
}
export const ThemeContext = createContext(INITIAL_STATE);
const ThemeContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(ThemeReducer, INITIAL_STATE);
return (
<ThemeContext.Provider value={{
isLightTheme: state.isLightTheme,
light: state.light,
dark: state.dark,
dispatch,
}}>
{children}
</ThemeContext.Provider>
);
}
export default ThemeContextProvider;
The ThemeReducer.js is:
const ThemeReducer = (state, action) => {
switch (action.type) {
case "SET_DARK":
return {
isLightTheme: false,
};
case "SET_LIGHT":
return {
isLightTheme: true,
};
default:
return state;
}
};
export default ThemeReducer;
app.js:
function App() {
return (
<div className="App">
<ThemeContextProvider>
<Navbar />
<BookList />
<ThemeToggle />
</ThemeContextProvider>
</div>
);
}
export default App;
And the ThemeToggle.js compoenent
const ThemeToggle = () => {
return (
<button onClick={()=>dispatch({type: "SET_DARK"})}>Change theme</button>
);
}
export default ThemeToggle;
However I get this error:
src/components/ThemeToggle.jsx
Line 6:30: 'dispatch' is not defined
I don't understand why. Because dispatch is supposed to be in the context. I'm wondering what is wrong here and how can I fix it?
P.S BooKList compoenent.
import { useContext } from 'react'
import { ThemeContext } from '../context/ThemeContext';
const BookList = () => {
const {isLightTheme, light, dark} = useContext(ThemeContext)
const theme = isLightTheme ? light : dark;
return (
<div style={{background : theme.ui , color: theme.syntax}}>
<ul>
<li stryle={{background: theme.ui}} >The way of kings</li>
<li stryle={{background: theme.ui}} >The namoe fot the wind</li>
<li stryle={{background: theme.ui}} >The Final empire</li>
</ul>
</div>
);
}

It appears you are missing accessing the ThemeContext in ThemeToggle. Use the useContext hook to access the ThemeContext Context value and destructure the dispatch function.
const ThemeToggle = () => {
const { dispatch } = useContext(ThemeContext);
return (
<button onClick={() => dispatch({type: "SET_DARK"})}>
Change theme
</button>
);
}
export default ThemeToggle;
And for completeness' sake, add dispatch to the default context value.
const INITIAL_STATE = {
isLightTheme: true,
light: {syntax: '#555', ui: '#ddd', bg: '#eee'},
dark: {syntax: '#ddd', ui: '#333', bg: '#555'},
dispatch: () => {},
}
export const ThemeContext = createContext(INITIAL_STATE);
The ThemeReducer reducer function is stripping out state in the set light/dark cases. You need to preserve the existing state.
const ThemeReducer = (state, action) => {
switch (action.type) {
case "SET_DARK":
return {
...state,
isLightTheme: false,
};
case "SET_LIGHT":
return {
...state,
isLightTheme: true,
};
default:
return state;
}
};

Related

React Native - Type Script: How to save dark mode toggle state even after turning off the application?

What is the correct way so that I can save the dark mode switch even after turning off the application?
I want to use the use-state-persist library to achieve the goal.
In my example I show app.tsx , Preferences.tsx, ViewField.tsx .
So that it will be possible to understand how the logic is built
DarkModeContext
import React from 'react';
interface DarkMode {
isDarkMode: boolean;
setDarkMode: () => void;
}
export const DarkModeContext = React.createContext<DarkMode>({} as DarkMode);
this is the app.tsx
import React, { useEffect, useState } from 'react';
import { syncStorage } from 'use-state-persist';
const App: () => ReactElement = () => {
const [isDarkMode, setDarkMode] = useState(false);
const Drawer = createDrawerNavigator();
const initStorage = async () => await syncStorage.init();
const toggleDarkMode = () => {
setDarkMode(!isDarkMode);
};
return (
<DarkModeContext.Provider value={{ isDarkMode, toggleDarkMode }}>
<NavigationContainer>
<Drawer.Navigator
drawerContent={SideMenu}
screenOptions={{
drawerPosition: 'right',
headerShown: false,
drawerType: 'front',
}}
>
<Drawer.Screen name='HomeScreen' component={StackNavigator} />
</Drawer.Navigator>
</NavigationContainer>
</DarkModeContext.Provider>
);
};
export default App;
this is the Preferences.tsx
import React, { useContext, useState } from 'react';
import ViewField from './ViewField';
import { DarkModeContext } from '~/context/DarkModeContext';
const Preferences = () => {
const { isDarkMode, toggleDarkMode } = useContext(DarkModeContext);
return (
<View>
<ViewField title='dark mode' isEnabled={isDarkMode} setValue={toggleDarkMode} />
</View>
);
};
export default Preferences;
this is the ViewField.tsx
import { View, Text, Switch } from 'react-native';
import React from 'react';
import styles from './ViewFieldStyles';
import { useContext } from 'react';
import { DarkModeContext } from '~/context/DarkModeContext';
type Props = {
title: string;
isEnabled: boolean;
setValue: () => void;
};
const ViewField = ({ title, isEnabled, setValue }: Props) => {
const { isDarkMode } = useContext(DarkModeContext);
return (
<View style={isDarkMode ? styles.optionViewDark : styles.optionView}>
<View style={styles.sameRowTextView}>
<Text style={isDarkMode ? styles.optionTextDark : styles.optionText}>{title}</Text>
<View style={styles.switchView}>
<Switch
trackColor={
isDarkMode
? { false: 'rgba(255, 255, 255, 0.38)', true: 'rgba(187, 134, 252, 0.38)' }
: { false: '#767577', true: 'rgba(4, 76, 163, 0.38)' }
}
onValueChange={setValue}
value={isEnabled}
/>
</View>
</View>
</View>
);
};
export default ViewField;
Keep in mind that there seems to be some problems in use-state-persist using Boolean values. Furthermore, the latest published version of this library is from 2020.
However, the use-state-persist library just seems to be a wrapper around AsyncStorage, which is very well maintained. I would encourage you to use this library instead.
In your case, this could be implemented as follows:
Store the actual setter of the state in the context,
Create an effect that accesses the async storage on mount of the application: if there exists a value for the corresponding key, set the state of the context, if not, then do nothing.
In the Preferences component, store a new state in the async storage as well.
const App: () => ReactElement = () => {
const [isDarkMode, setDarkMode] = useState(false);
const contextValue = React.useMemo(() => ({
isDarkMode,
setDarkMode
}), [isDarkMode])
React.useEffect(() => {
const load = async () => {
const value = await AsyncStorage.getItem('isDarkMode');
if (value !== null) {
setDarkMode(JSON.parse(value));
}
}
load();
}, [])
return (
<DarkModeContext.Provider value={contextValue}>
...
};
In the Preferences component, set the state and save it to the local storage.
const Preferences = () => {
const { isDarkMode, setDarkMode } = useContext(DarkModeContext);
async function toggle() {
const newValue = JSON.stringify(!isDarkMode);
await AsyncStorage.setItem('isDarkMode', newValue);
setDarkMode(prev => !prev);
}
return (
<View>
<ViewField title='dark mode' isEnabled={isDarkMode} setValue={toggle} />
</View>
);
}

useSelector doesn't update value in Next.js

I have a problem, createAsyncThunk function makes request to server (axios) and then get data, after that extraReducers handle builder.addCase in it and makes state.value = action.payload, then console.log(state.value) writes value from server. Great! It works, but when I use useSelector it sees existing value from initialState but get value only when it was first time initialized (null or just []) not updated after dispatch in wrapper.getServerSIdeProps. Same with just reducers and function in it. It works change state (console.log write it) but useSelector doesn't give me updated value.
UPDATE:
If you have same issue.
Just give up, don't use next-redux-wrapper. Context Api.
Slice code
import { createAsyncThunk, createSlice, PayloadAction } from '#reduxjs/toolkit';
import axios from 'axios';
import { HYDRATE } from 'next-redux-wrapper';
// types
import { IInitialStateV1 } from '../types/store';
import { ITrack } from '../types/tracks/track';
export const fetchData = createAsyncThunk('main/fetchData', async (): Promise<ITrack[]> => {
const { data } = await axios.get<ITrack[]>('http://localhost:5000/track');
return data;
})
const initialState: IInitialStateV1 = {
pause: true,
currentTime: 0,
volume: 0,
duration: 0,
active: null,
tracks: [],
}
export const mainSlice = createSlice({
name: 'main',
initialState,
reducers: {
setPause(state, action: PayloadAction<boolean>) {
state.pause = action.payload;
},
setTime(state, action: PayloadAction<number>) {
state.currentTime = action.payload;
},
setVolume(state, action: PayloadAction<number>) {
state.volume = action.payload;
},
setDuration(state, action: PayloadAction<number>) {
state.duration = action.payload;
},
setActive(state, action: PayloadAction<ITrack>) {
state.active = action.payload;
state.currentTime = 0;
state.duration = 0;
}
},
extraReducers: (builder) => {
// [HYDRATE]: (state, action) => {
// return {
// ...state,
// ...action.payload,
// }
// },
// [fetchData.fulfilled.toString()]: (state, action: PayloadAction<ITrack[]>) => {
// state.tracks = action.payload;
// }
builder.addCase(HYDRATE, (state, action: any) => {
return {
...state,
...action.payload,
}
}).addCase(fetchData.fulfilled, (state, action: PayloadAction<ITrack[]>) => {
// return {
// ...state,
// ...action.payload,
// }
state.tracks = action.payload;
});
}
})
export const { setPause, setTime, setVolume, setDuration, setActive } = mainSlice.actions;
export default mainSlice.reducer;
configurate store
import { AnyAction, configureStore, ThunkDispatch } from '#reduxjs/toolkit';
import { createWrapper, MakeStore, Context } from 'next-redux-wrapper';
import mainRed from './index';
const makeStore = () => configureStore({
reducer: {
main: mainRed
},
})
type AppStore = ReturnType<typeof makeStore>;
export type RootState = ReturnType<AppStore['getState']>;
export type AppDispatch = AppStore['dispatch'];
export type NextThunkDispatch = ThunkDispatch<RootState, void, AnyAction>;
export const wrapper = createWrapper<AppStore>(makeStore);
hooks for TypeScript
import { useDispatch, useSelector, TypedUseSelectorHook } from "react-redux";
import { RootState, AppDispatch } from "../store/reducer";
export const useTypeSelector: TypedUseSelectorHook<RootState> = useSelector;
export const useTypeDispath = ()=> useDispatch<AppDispatch>();
getServerSideProps and useSelector (in page)
import { Container, ListItem, Stack, Box, Button } from "#mui/material";
import TrackList from "../../components/TrackList";
// interfaces
import { ITrack } from "../../types/tracks/track";
// import hooks
import { useRouter } from "next/router";
import { useTypeSelector } from "../../hooks/useTypeSelector";
// wrapper
import { NextThunkDispatch, wrapper } from "../../store/reducer";
import { fetchData, setVolume } from "../../store";
export default function Index(): JSX.Element {
const router = useRouter();
const tracks: ITrack[] = useTypeSelector(state => state.main.tracks);
return (
<div className="main">
<Container >
<Stack marginTop={20} sx={{ backgroundColor: "#C4C4C4", fontSize: '24px', fontWeight: 'bold' }}>
<Box p={5} justifyContent="space-between">
<ListItem>List of Tracks</ListItem>
<Button variant="outlined" sx={{ backgroundColor: 'blue', color: 'white' }} onClick={() => router.push('/tracks/create')} >Upload</Button>
</Box>
<TrackList tracks={tracks} />
</Stack>
</Container>
</div>
)
}
export const getServerSideProps = wrapper.getServerSideProps((store) => async () => {
const dispatch = store.dispatch as NextThunkDispatch;
// dispatch(fetchData());
dispatch(setVolume(2));
return {
props: {}
}
})

State is not triggering re-render after change in state

I am combining two reducers in my React app. One of them is working fine, but one is not triggering re-render after a change in state. But after when I save document or make change in other reducer state, component re-renders for both reducers
reducer which is not working fine:
import {
SET_ACTIVE_SUBJECT,
SET_ACTIVE_TOPIC
} from '../action/action-types'
const initialState = {
activeSubject: '',
activeTopic: ''
}
const makeState = (stateActiveSubject, stateActiveTopic) => {
return {
activeSubject: stateActiveSubject,
activeTopic: stateActiveTopic
}
}
export const uireducer = (state = initialState, action) => {
switch(action.type){
case SET_ACTIVE_SUBJECT:
// this statement is printing new state in console, but not triggering re-render
console.log('New State : ', makeState(action.payload,''));
return makeState(action.payload,'')
case SET_ACTIVE_TOPIC:
return makeState(state.uireducer.activeSubject,action.payload)
default:
return state
}
}
Component which is not re-rendering:
const Topics = ({activeSubject, data}) => {
const classes = useStyles()
const [topics, setTopics] = useState([])
useEffect(() => {
console.log('Active Subject : ', activeSubject);
if(activeSubject){
console.log('Data : ', data.filter(subject => (subject.id === activeSubject))[0].topics);
setTopics(data.filter(subject => (subject.id === activeSubject))[0].topics)
}
}, [])
return (
<List>
{
topics.length > 0 ? topics.map(topic => {
return (
<ListItem button className={classes.listItem} id={topic.id} key={topic.id}>
{topic.name}
<ButtonGroup className={classes.editDelete}>
<IconButton className={classes.icon}>
<Edit />
</IconButton>
<IconButton className={classes.icon}>
<Delete />
</IconButton>
</ButtonGroup>
</ListItem>
)
}) : <div className={classes.waitMessage}><p>Select Subject To Display Topics</p></div>
}
</List>
)
}
const mapStateToProps = state => ({
activeSubject: state.uireducer.activeSubject,
data: state.subjects.data
})
const mapDispatchToProps = dispatch => ({
})
export default connect(mapStateToProps, mapDispatchToProps)(Topics);
reducer which is working fine:
import {
FETCHING_DATA,
FETCHED_DATA,
FETCH_DATA_ERROR
} from '../action/action-types'
const initialState = {
isDataFetching : false,
error: '',
data: [],
}
const makeState = (dataFetching, stateError, stateData) => {
return {
isDataFetching: dataFetching,
error: stateError,
data: stateData,
}
}
export const reducer = (state = initialState, action) => {
switch(action.type){
case FETCHING_DATA:
return makeState(true,'',[])
case FETCHED_DATA:
return makeState(false,'',action.payload)
case FETCH_DATA_ERROR:
return makeState(false,action.payload,[])
default:
return state
}
}
Store here :
import { createStore, combineReducers, applyMiddleware } from 'redux'
import thunk from 'redux-thunk';
import {reducer} from '../reducer/reducer'
import { uireducer } from '../reducer/uireducer'
console.log(reducer);
const rootReducer = combineReducers({
subjects: reducer,
uireducer
})
const store = createStore(rootReducer, applyMiddleware(thunk))
export default store
Set activeSubject as dependency for the useEffect, so it will rerun when the value changes.
useEffect(() => {
// ...
}, [activeSubject])

React components not rendering from global state using context API

I am trying to get my child component to look for the global state. As of now, I can log out my global state, I can see that it has been updated, but my components are not updating with the new value. I am not getting any errors but also have had a hard time find a good solution. I have the full repo(very small with only 1 component) if it helps here: https://github.com/jaysnel/context-api-react
Is there any reason my ChangeColor.js file is not updating with the new state change?
UpdateColor.js
import React, { useReducer, useEffect } from 'react'
import { useAppContext } from '../public/context/context'
export default function UpdateColor() {
let { myGlobaData } = useAppContext();
const [state, dispatch] = useReducer(reducer, myGlobaData);
function reducer(state, action) {
let newState = state.slice();
console.log(state);
if(newState !== undefined) {
switch(action.type) {
case 'UPDATE_COLOR':
newState[0].color = 'green';
return newState;
default:
throw new Error();
}
}
}
return (
<div>
<button onClick={() => dispatch({type: 'UPDATE_COLOR'})}>Update Color</button>
</div>
)
}
ChangeColor.js
import React from 'react'
import { useAppContext } from '../public/context/context'
export default function ChangeColor() {
const { myGlobaData } = useAppContext();
console.log("ChangeColor.js", myGlobaData)
return (
<div>
<h2 style={{color: myGlobaData[0].color}}>I Change Colors Based On Global State.</h2>
</div>
)
}
context.js
import { createContext, useContext } from 'react';
const AppContext = createContext();
export function AppWrapper({ children }) {
const state = {
myGlobaData: [
{
color: 'red',
text: 'new text new me'
}
],
}
return (
<AppContext.Provider value={state}>
{children}
</AppContext.Provider>
);
}
export function useAppContext() {
return useContext(AppContext);
}
I guess, your issue where did you used the reducer. Component ChangeColor.js don't know what are you doing inside UpdateColor.js. The solution is if put the reducer into global context context.js and then you have to acsess your reducer globally.
I did created and pushed to github working example with two different approaches to using an actions reducer. working example
UpdateColor.js
import { useAppContext } from '../public/context/context'
export default function UpdateColor() {
const { dispatch } = useAppContext();
const withPayload = () => {
dispatch({
type: 'UPDATE_COLOR_WITH_PAYLOAD',
payload: {color: 'blue', text: 'new text from updateColor.js'}})
}
const intoReducer = () => {
dispatch({type: 'UPDATE_COLOR_INTO_REDUCER'})
}
return (
<div>
<button onClick={withPayload}>Update Color with payload</button>
<button onClick={intoReducer}>Update Color into reducer</button>
</div>
)
}
ChangeColor.js
import { useAppContext } from '../public/context/context'
export default function ChangeColor() {
const { state } = useAppContext();
return (
<div>
<h2 style={{color: state.color}}>I Change Colors Based On Global State.</h2>
<p style={{textAlign: 'center'}}>{state.text}</p>
</div>
)
}
context.js
import { createContext, useContext, useReducer } from 'react';
const AppContext = createContext();
export function useAppContext() {
return useContext(AppContext);
}
function reducer(state, action) {
switch (action.type) {
case 'UPDATE_COLOR_WITH_PAYLOAD':
return action.payload;
case 'UPDATE_COLOR_INTO_REDUCER':
action.color = 'green';
action.text = 'new text from reducer';
return action;
default:
return state;
}
}
export function AppWrapper({ children }) {
const initialState = { color: 'red', text: 'new text new me' };
const [state, dispatch] = useReducer(reducer, initialState);
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
}

is there any way to make this react component less verbose?

So, i wrote a test project to explore react, react-router and react-redux.
After i got everything working fine i laid my eyes again on Settings.jsx and i am wondering how could i make it less verbose and error prone:
import React, { Component } from "react";
import { connect } from "react-redux";
class Settings extends Component {
state = { name: this.props.settings.name };
render() {
return (
<div>
<h1>Settings</h1>
<p>This is Settings page</p>
My name is{" "}
<input
value={this.state.name}
onChange={e => this.setState({ name: e.target.value })}/>
<button onClick={e => this.props.changeName(this.state.name)}>
Change
</button>
</div>
);
}
}
const mapState = state => ({ settings: state.settings });
const mapDispatch = dispatch => {
return {
changeName(name) {
dispatch({ type: "setName", name });
}
};
};
export default connect(
mapState,
mapDispatch
)(Settings);
My first idea was to convert it into a functional component, but it's said that they don't have state and i need the state to locally handle the input.
With #babel/plugin-proposal-decorators, connect can be used as a decorator:
import React, { Component } from "react";
import { connect } from "react-redux";
const mapState = state => ({ settings: state.settings });
const mapDispatch = dispatch => {
return {
changeName(name) {
dispatch({ type: "setName", name });
}
};
};
#connect(mapState, mapDispatch)
export default class Settings extends Component {
state = { name: this.props.settings.name };
render() {
return (
<div>
<h1>Settings</h1>
<p>This is Settings page</p>
My name is{" "}
<input
value={this.state.name}
onChange={e => this.setState({ name: e.target.value })}/>
<button onClick={e => this.props.changeName(this.state.name)}>
Change
</button>
</div>
);
}
}
small, but imho nice simplification
also, you could use concise syntax with your mapDispatch:
const mapDispatch = dispatch => ({
changeName(name) {
dispatch({ type: "setName", name });
}
});
you can do this if you want to to add the typing text in store:
Settings.js
import React from "react";
import { changeName, typingName } from '../actions/settingsActions'
import { connect } from "react-redux";
const Settings = () => {
const { changeName, typingName, typedName, submittedName } = this.props
return (
<div>
<h1>Settings</h1>
<p>This is Settings page</p>
My name is{" "}
<input
value={typedName}
onChange={e => typingName(e.target.value)}/>
<button onClick={changeName(submittedName)}>
Change
</button>
</div>
);
}
const mapState = state => ({
typedName: state.typedName,
submittedName: state.submittedName
});
const mapDispatchToProps = dispatch => ({
typingName: x => dispatch(typingName(x)),
changeName: x => dispatch(changeName(x))
})
export default connect(
mapState,
mapDispatch
)(Settings);
settingsActions.js
export const typingName = payload => ({
type: 'TYPING_NAME',
payload
});
export const changeName = payload => ({
type: 'CHANGE_NAME',
payload
});
settingsReducer.js
export const typingName = (state = [], action) => {
switch (action.type) {
case 'TYPING_NAME':
return [...state, action.payload];
default:
return state;
}
};
export const changeName = (state = '', action) => {
switch (action.type) {
case 'CHANGING_NAME':
return action.payload;
default:
return state;
}
};
You could maybe achieve something like this. But validating the typing state inside the component then sending the final result to the store as you did is a better idea I think, to avoid so much verbose.
Also you should of course create a constants file, but I guess you know already.

Categories

Resources