I'm trying to use createContext with useReducer and have some trouble
I'm dispatching two actions and both actions are storing their payload at the same place in the state, not to their own.
All the help will be appreciated
Here is my store
import React, { createContext, useReducer, Dispatch } from 'react';
import { InitialStateType } from './types';
import { citiesReducer, CitiesActions } from './citiesReducer';
import { LoadingActions, loadingReducer } from './loadingReducer';
const initialState: InitialStateType = {
cities: [],
loading: false,
};
const store = createContext<{
state: InitialStateType;
dispatch: Dispatch<CitiesActions | LoadingActions>;
}>({
state: initialState,
dispatch: () => {},
});
const { Provider } = store;
const mainReducer = (
{ cities, loading }: InitialStateType,
action: LoadingActions | CitiesActions,
) => ({
cities: citiesReducer(cities, action),
loading: loadingReducer(loading, action),
});
const StateProvider = ({ children }: any): React.ReactElement => {
const [state, dispatch] = useReducer<any>(mainReducer, initialState);
return <Provider value={{ state, dispatch }}>{children}</Provider>;
};
export { store, StateProvider };
Both reducers
import { ActionTypes } from './types';
export type CitiesActions = {
type: ActionTypes.SET_CITIES_DATA;
payload: [];
};
export const citiesReducer = (state: [], action: CitiesActions) => {
switch (action.type) {
case action.type:
return (state = action.payload);
default:
return state;
}
};
import { ActionTypes } from './types';
export type LoadingActions = {
type: ActionTypes.LOADING;
payload: boolean;
};
export const loadingReducer = (state: boolean, action: LoadingActions) => {
switch (action.type) {
case action.type:
return (state = action.payload);
default:
return state;
}
};
Here I'm dispatching the actions one after another
dispatch({ type: ActionTypes.SET_CITIES_DATA, payload: result });
dispatch({ type: ActionTypes.LOADING, payload: false });
And as a result, I'm getting in my state
cities: false
loading: false
instead of
cities: [data],
loading: false
You need to specify the action when handling reducers instead of having a case like case action.type in switch statement otherwise regardess of what action you dispatch all reducers will use it and set the payload. In such a case the last actions data will be set for all states
export type CitiesActions = {
type: ActionTypes.SET_CITIES_DATA;
payload: [];
};
export const citiesReducer = (state: [], action: CitiesActions) => {
switch (action.type) {
case ActionTypes.SET_CITIES_DATA: // specify the action here
return (state = action.payload);
default:
return state;
}
};
import { ActionTypes } from './types';
export type LoadingActions = {
type: ActionTypes.LOADING;
payload: boolean;
};
export const loadingReducer = (state: boolean, action: LoadingActions) => {
switch (action.type) {
case ActionTypes.LOADING: // Specify the action here
return (state = action.payload);
default:
return state;
}
};
Related
So I tried of implementing Typescript into React.JS for the first time. However, I came to a problem with the Context API. It seems that I can't get actual correct initial values.
For example: initialValue are: {location: "asdasd", isLoading: false}
but it still returns me {location: "", isLoading: false}
Here is the code:
First of all I implemented reducer part for changing state
export const enum WeatherContants {
SET_LOCATION = 'SET_LOCATION',
SET_IS_LOADING = 'SET_IS_LOADING',
}
export type WeatherReducerType = {
location: string;
isLoading: boolean;
}
type ActionType = | {type: "SET_LOCATION"; payload: string} | {type: "SET_IS_LOADING", payload: boolean}
const weatherReducer = (state: WeatherReducerType, action: ActionType) => {
switch (action.type) {
case WeatherContants.SET_LOCATION:
return {
...state,
location: action.payload,
}
case WeatherContants.SET_IS_LOADING:
return {
...state,
isLoading: action.payload,
}
default:
return {...state}
}
}
export default weatherReducer;
Here is the context creation part as well as context provider.
import React, {createContext} from 'react';
import useWeather from './useWeather';
import { UseWeatherResult } from './useWeather';
import { WeatherReducerType } from './weatherReducer';
export const WeatherContext = createContext<UseWeatherResult>({
weatherState: {
location: "",
isLoading: false,
},
setLocation: () => {},
setIsLoading: () => {},
});
export const WeatherProvider: React.FunctionComponent<{initialValues: WeatherReducerType; children?: React.ReactNode}> = ({initialValues, children}) => {
return (
<WeatherContext.Provider value={useWeather(initialValues)}>
{JSON.stringify(useWeather(initialValues))}
{children}
</WeatherContext.Provider>
)
}
At the end, custom hook, which stores functions for changing states as well as state.
import {useReducer} from 'react';
import { WeatherReducerType, WeatherContants } from './weatherReducer';
import weatherReducer from './weatherReducer';
export type UseWeatherResult = ReturnType<typeof useWeather>;
const useWeather = (initialState: WeatherReducerType): {
weatherState: WeatherReducerType;
setLocation: (location: string) => void;
setIsLoading: (condition: boolean) => void;
} => {
const [weatherState, dispatch] = useReducer(weatherReducer, initialState);
const setLocation = (location: string) => dispatch({type: WeatherContants.SET_LOCATION, payload: location});
const setIsLoading = (condition: boolean) => dispatch({type: WeatherContants.SET_IS_LOADING, payload: condition});
return {
weatherState,
setLocation,
setIsLoading,
}
}
export default useWeather;
I have two reducers inside a combine reducer, and when I call an action to change the state of reducerSrc, my state of reducerTherm get undefined and I receive an error. How can I fix this?
combine:
const rootReducer = combineReducers({
therm: reducerTherm,
src: reducerSrc,
});
export type RootState = ReturnType<typeof rootReducer>;
export const store = createStore(rootReducer);
reducerTherm:
interface Props {
therm: string;
}
const initialState: Props = {
therm: "",
};
export default function reducerTherm(state: Props = initialState, action: any) {
switch (action.type) {
case "SEARCH_THERM":
return action.payload;
break;
default:
return state.therm;
}
}
reducerSrc:
export interface Props {
src: any;
}
const initialState: Props = {
src: "source teste",
};
export default function reducerSrc(state: Props = initialState, action: any) {
switch (action.type) {
case "ADD_SRC":
return action.payload;
break;
default:
return state;
}
}
useEffect watching the therm changing and with this active the action to reducerSrc:
const therm = useSelector((state: RootState) => state.therm);
const src = useSelector((state: RootState) => state.src);
useEffect(() => {
if (therm) {
try {
getPhotosPlaces("shopping")
.then((res) => res.json())
.then((data) => {
store.dispatch({
type: "ADD_SRC",
payload: data,
});
});
} catch (error) {
console.log(error);
}
}
}, [therm]);
The error:
**Unhandled Rejection (Error): When called with an action of type "ADD_SRC", the slice reducer for key "therm" returned undefined. To ignore an action, you must explicitly return the previous state. If you want this reducer to hold no value, you can return null instead of undefined.**
In the default case of your therm reducer you have
return state.therm; // possibly undefined
Surely you mean to be returning the reducer state (not the key therm)
return state; // returns { therm: ... }
Instead of any, I wanna use proper TS types in the following code. I 'm new to react TS, pls help...
How do I set typescript types for useReducer useContext for the following context API code:
import React, {createContext, Dispatch} from 'react';
import {firebaseUser} from '../#types/User';
interface Actions {
SET_IMAGENAME: string;
SET_USER: string;
}
export const Actions: Actions = {
SET_IMAGENAME: 'SET_IMAGENAME',
SET_USER: 'SET_USER',
};
function action(type: string) {
return {type};
}
function actionPayload(type: string, payload: any) { //here
return {type, payload};
}
export const Dispatches = {
setImageName: action,
setUser: actionPayload,
};
interface State {
imgName: string;
user: firebaseUser;
}
const initialState = {
imgName: '',
user: {} as firebaseUser,
};
function reducer(state = initialState, action: {type: string; payload: any}) { //here
switch (action.type) {
case Actions.SET_IMAGENAME:
return {...state, imgName: 'sample image'};
case Actions.SET_USER:
return {...state, user: action.payload};
default:
return state;
}
}
export const Store = createContext<{
state: State;
dispatch: Dispatch<any>; //here
}>({
state: initialState,
dispatch: () => null,
});
export function StoreProvider({children}: JSX.ElementChildrenAttribute): JSX.Element {
const [state, dispatch] = React.useReducer(reducer, initialState);
return <Store.Provider value={{state, dispatch}}>{children}</Store.Provider>;
}
Could Anyone help me It will be appreciated?
Thank You
I hope this solution will give good idea.
https://gist.github.com/sw-yx/f18fe6dd4c43fddb3a4971e80114a052
https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/context/#extended-example
export function createCtx<StateType, ActionType>(
reducer: React.Reducer<StateType, ActionType>,
initialState: StateType,
) {
const defaultDispatch: React.Dispatch<ActionType> = () => initialState // we never actually use this
const ctx = React.createContext({
state: initialState,
dispatch: defaultDispatch, // just to mock out the dispatch type and make it not optioanl
})
function Provider(props: React.PropsWithChildren<{}>) {
const [state, dispatch] = React.useReducer<React.Reducer<StateType, ActionType>>(reducer, initialState)
return <ctx.Provider value={{ state, dispatch }} {...props} />
}
return [ctx, Provider] as const
}
// usage
const initialState = { count: 0 }
type AppState = typeof initialState
type Action =
| { type: 'increment' }
| { type: 'add'; payload: number }
| { type: 'minus'; payload: number }
| { type: 'decrement' }
function reducer(state: AppState, action: Action): AppState {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
case 'add':
return { count: state.count + action.payload }
case 'minus':
return { count: state.count - action.payload }
default:
throw new Error()
}
}
const [ctx, CountProvider] = createCtx(reducer, initialState)
export const CountContext = ctx
// top level example usage
export function App() {
return (
<CountProvider>
<Counter />
</CountProvider>
)
}
// example usage inside a component
function Counter() {
const { state, dispatch } = React.useContext(CountContext)
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'add', payload: 5 })}>+5</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'minus', payload: 5 })}>+5</button>
</div>
)
}
It's really depended on what can the payload contain, it can contain string for image or FirebaseUser instance you can set payload: string | FirebaseUser
import React, {createContext, Dispatch} from 'react';
import {firebaseUser} from '../#types/User';
interface Actions {
SET_IMAGENAME: string;
SET_USER: string;
}
export const Actions: Actions = {
SET_IMAGENAME: 'SET_IMAGENAME',
SET_USER: 'SET_USER',
};
function action(type: string) {
return {type};
}
// If payload can contain string of image or FirebaseUser instance
// it will be string | FirebaseUser
// if payload will only contain FirebaseUser instance you just need payload: FirebaseUser
export type ActionType = {
type: string,
payload: string | FirebaseUser
}
// If payload can contain string of image or FirebaseUser instance
// it will be string | FirebaseUser
// if payload will only contain FirebaseUser instance you just need payload: FirebaseUser
function actionPayload(type: string, payload: string | FirebaseUser ) { //here
return {type, payload};
}
export const Dispatches = {
setImageName: action,
setUser: actionPayload,
};
interface State {
imgName: string;
user: FirebaseUser;
}
const initialState = {
imgName: '',
user: {} as firebaseUser,
};
// set Action type here
function reducer(state = initialState, action: ActionType) {
switch (action.type) {
case Actions.SET_IMAGENAME:
// need to cast type herer
return {...state, imgName: action.payload as string};
case Actions.SET_USER:
// need to cast type herer
return {...state, user: action.payload as firebaseUser};
default:
return state;
}
}
export const Store = createContext<{
state: State;
dispatch: Dispatch<ActionType>; //action type here
}>({
state: initialState,
dispatch: () => null,
});
export function StoreProvider({children}: JSX.ElementChildrenAttribute): JSX.Element {
const [state, dispatch] = React.useReducer(reducer, initialState);
return <Store.Provider value={{state, dispatch}}>{children}</Store.Provider>;
}
Here is also my solution by adding types file to do more cleanness to your file structures here is it and I hope you find it useful :
myCodesandboxRepo
How do I get the state of this? I only need to put it as false and true the time I want at my components, but i`m doing something wrong, i know how do it when calling an API, but not like this.
I have this actions:
import { HIDE_MENU, ESTADO_MENU } from "./types";
export const hideMenu = dispatch => {
return dispatch({
type: HIDE_MENU
});
};
export const estadoDoMenu = open => dispatch => {
dispatch({
type: ESTADO_MENU
});
};
and this reducer:
import { HIDE_MENU, ESTADO_MENU } from "../actions/types";
const initialState = {
open: true
};
export default function(state = initialState, action) {
switch (action.type) {
case HIDE_MENU:
return {
...state,
open: false
};
case ESTADO_MENU:
console.log("chega aqui");
return {
...state
};
default:
return state;
}
}
but calling it like this:
componentDidMount() {
console.log("Estado do Menu: ", this.props.estadoDoMenu());
}
I get undefined at the console, what is wrong?
I am using multiple reducers in my project and then combining them with combineReducers() function and have all actions in single file. when i dispatch the action, it is returning me state values to undefined. I think It can't find out because of multiple reducerse. But when i use single reducer file. It is working fine. Can anyone please tell me what the issue.It is how i am combining the reducers.
const rootReducer = combineReducers({
isMobileReducer,
imageSliderReducer
})
and now passing to store, like below:
let store = createStore(rootReducer,applyMiddleware(thunk))
and in frontend how i am accessing state
const mapStateToProps = (state) => ({
images: state.images,
isMobile: state && state.isMobile
})
imageSliderReducer.js
import {
FETCH_IMAGES_BEGIN,
FETCH_IMAGES_SUCCESS,
FETCH_IMAGES_FAILURE
} from '../actions/actionTypes'
const initialState = {
images:[],
error:null
}
const imageSliderReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_IMAGES_BEGIN:
return {...state,error:null}
case FETCH_IMAGES_SUCCESS:
return {...state,images:action.payload.images}
case FETCH_IMAGES_FAILURE:
return {...state,error:action.payload.error,images:[]}
default:
return state
}
}
export default imageSliderReducer;
isMobileReducer.js
import {
OPEN_MENU,
CLOSE_MENU,
SET_DEVICE_TYPE,
} from '../actions/actionTypes'
const initialState = {
isMenuOpen: null,
isMobile: false
}
const isMobileReducer = (state = initialState, action) => {
switch (action.type) {
case OPEN_MENU:
return {...state, isMenuOpen: true}
case CLOSE_MENU:
return {...state, isMenuOpen: false}
case SET_DEVICE_TYPE:
return {...state, isMobile: action.isMobile}
default:
return state
}
}
export default isMobileReducer;
actionCreator.js
import {
OPEN_MENU,
CLOSE_MENU,
SET_DEVICE_TYPE,
FETCH_IMAGES_BEGIN,
FETCH_IMAGES_SUCCESS,
FETCH_IMAGES_FAILURE
} from './actionTypes'
export function openMenu(isMobile) {
return {
type: OPEN_MENU
}
}
export function closeMenu(isMobile) {
return {
type: CLOSE_MENU
}
}
export function setDeviceType (isMobile) {
return {
type: SET_DEVICE_TYPE,
isMobile: isMobile
}
}
export function fetchImages() {
return dispatch => {
dispatch(fetchImagesBegin());
return fetch("https://7344.rio.com/wp-json/customapi/homeslider")
.then(handleErrors)
.then(res => res.json())
.then(json => {
dispatch(fetchImagesSuccess(json.posts));
return json.posts;
})
.catch(error => dispatch(fetchImagesFailure(error)));
};
}
function handleErrors(response) {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
}
export const fetchImagesBegin = () => ({
type: FETCH_IMAGES_BEGIN
});
export const fetchImagesSuccess = images => ({
type: FETCH_IMAGES_SUCCESS,
payload: { images }
});
export const fetchImagesFailure = error => ({
type: FETCH_IMAGES_FAILURE,
payload: { error }
});
Try using this:
const mapStateToProps = (state) => ({
images: state.imageSliderReducer.images,
isMobile: state.isMobileReducer.isMobile
})