I'm trying to build a generic Store using React's useReducer and useContext but I'm having an issue with the inference of the default state.
The store generator function is the following:
export function generateStore<Actions extends ReducerAction, State = any>(defaultValue: State, reducer: (state: State, action: Actions) => State): {
provider: (props: { children: ReactNode }) => ReactElement;
dispatcher: (action: Actions) => void;
useStore: () => State;
} {
const store = createContext(defaultValue);
const { Provider } = store;
let dispatch: React.Dispatch<Actions>;
const ProviderElm = (props: { children: ReactNode }): ReactElement => {
const { children } = props;
const [state, dispatcher] = useReducer(reducer, defaultValue);
dispatch = dispatcher;
return <Provider value={state}>{children}</Provider>;
};
return {
provider: ProviderElm,
dispatcher: (action: Actions) => dispatch && dispatch(action),
useStore: () => useContext(store),
};
}
An initializer example could be:
const defaultState = {
auth: {
authenticated: false,
},
};
type StoreActions =
| {
type: 'LOGIN';
payload: {
token: string;
};
}
| {
type: 'LOGOUT';
};
const { dispatcher, provider, useStore } = generateStore<StoreActions>(
defaultState,
(state = defaultState, action) => {
switch (action.type) {
case 'LOGIN': {
const { token } = action.payload;
return {
...state,
auth: {
authenticated: true,
token,
},
};
}
case 'LOGOUT': {
return {
...state,
auth: {
authenticated: false,
token: null,
},
};
}
default:
return defaultState;
}
},
);
The issue is that the State generic of generateStore can't infer itself as the typeof the parameter defaultValue.
It always requires me to initialize it like this or else the intellisense won't work out the type:
generateStore<StoreActions, typeof defaultState>
Any idea on how I make this work and why it currently can't infer the type?
If you want TypeScript to infer your generic types. You cannot provide any type arguments to the function. TypeScript does not support partial type inference. It's all or nothing. By calling generateStore<StoreActions> you are triggering the compiler to use the predefined State = any generic argument on your function.
I would recommend having a strongly typed state to make it cleaner.
type State = {
auth: {
authenticated: boolean
}
}
type StoreActions =
| {
type: 'LOGIN';
payload: {
token: string;
};
}
| {
type: 'LOGOUT';
};
const defaultState: State = {
auth: {
authenticated: false,
},
};
const { dispatcher, provider, useStore } = generateStore<StoreActions, State>(
defaultState,
(state = defaultState, action) => {
switch (action.type) {
case 'LOGIN': {
const { token } = action.payload;
return {
...state,
auth: {
authenticated: true,
token,
},
};
}
case 'LOGOUT': {
return {
...state,
auth: {
authenticated: false,
token: null,
},
};
}
default:
return defaultState;
}
},
);
The only other option is to create a wrapper function that only needs one argument to infer (the state) and supplies the actions type directly. You'll need one for each set of actions, but it might be a good work around depending on how many times it will be used.
type StoreActions =
| {
type: 'LOGIN';
payload: {
token: string;
};
}
| {
type: 'LOGOUT';
};
const defaultState = {
auth: {
authenticated: false,
},
};
export function generateStoreWithStoreActions<State = any>(defaultValue: State, reducer: (state: State, action: StoreActions) => State) {
return generateStore<StoreActions, State>(defaultValue, reducer);
}
const { dispatcher, provider, useStore } = generateStoreWithStoreActions(
defaultState,
(state = defaultState, action) => {
switch (action.type) {
case 'LOGIN': {
const { token } = action.payload;
return {
...state,
auth: {
authenticated: true,
token,
},
};
}
case 'LOGOUT': {
return {
...state,
auth: {
authenticated: false,
token: null,
},
};
}
default:
return defaultState;
}
},
);
Related
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
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;
}
};
Summary
In order to learn Redux, I am incorporating some state, actions, reducers, and trying to see how they are used in React Components.
I have set up a test object...
const initialState = {
navigationCount : 0,
someNumber : 500,
someList : ['aa',22,'c5d6','45615'],
};
...and aim to:
increment the navigationCount by 1 when visiting pages
add or subtract from someNumber
push() & pop() elements from someList.
Versions
Currently using gatsby ^2.5.0, react ^16.8.6, and react-redux ^6.0.1.
Code
actions & reducers
import { combineReducers } from 'redux';
import {
PAGE_INCREMENT,
NUMBER_INCREASE,
NUMBER_DECREASE,
LIST_PUSH,
LIST_POP,
} from './actionTypes.js';
// state
const initialState = {
navigationCount : 0,
someNumber : 500,
someList : ['aa',22,'c5d6','45615'],
};
// action creators returning actions
export const pageIncrementer = navigationCount => {
return {
type: PAGE_INCREMENT,
navigationCount,
};
};
export const numberAdder = numberToAdd => {
return {
type: NUMBER_INCREASE,
numberToAdd,
};
};
export const numberMinuser = numberToMinus => {
return {
type: NUMBER_DECREASE,
numberToMinus,
};
};
export const listPusher = itemToAdd => {
return {
type: LIST_PUSH,
itemToAdd,
}
};
export const listPopper = () => {
return {
type: LIST_POP,
}
};
// reducers
const pageIncrementReducer = (state = initialState, action) => {
switch (action.type) {
case PAGE_INCREMENT:
return Object.assign({}, ...state, {
navigationCount: action.navigationCount+1
});
default:
return state.navigationCount;
}
};
const numberChanger = (state = initialState, action) => {
switch (action.type) {
case NUMBER_INCREASE:
return Object.assign({}, ...state, {
someNumber: state.someNumber+action.numberToAdd,
});
case NUMBER_DECREASE:
return Object.assign({}, ...state, {
someNumber: state.someNumber-action.numberToMinus,
});
default:
return state.someNumber;
};
};
const listChanger = (state = initialState, action) => {
switch (action.type) {
case LIST_POP:
return Object.assign({}, ...state, {
someList: state.someList.pop(),
});
case LIST_PUSH:
return Object.assign({}, ...state, {
someList: state.someList.push(action.itemToAdd),
});
default:
return state.someList;
}
}
// store
const rootReducer = combineReducers({
pageIncrementReducer,
numberChanger,
listChanger,
});
export default rootReducer;
React Component
import React from 'react';
import Layout from '../components/common/Layout.jsx';
import LandingBanner from '../components/landing/LandingBanner.jsx';
import LandingNavgrid from '../components/landing/LandingNavgrid.jsx';
import LandingApp from '../components/landing/LandingApp.jsx';
import { connect } from 'react-redux';
import {
PAGE_INCREMENT,
NUMBER_INCREASE,
NUMBER_DECREASE,
LIST_PUSH,
LIST_POP,
} from '../state/actionTypes';
class LandingPage extends React.Component {
constructor(props){
super(props);
this.state = {
appliedNum: 2000,
};
}
componentDidMount(){
// this.props.pageIncrement(); // => numberChanger returned undefined
// this.props.numberIncrease(4444); // => pageIncrementReducer returned undefined
// this.props.numberDecrease(4444); // => pageIncrementReducer returned undefined
// this.props.listPush(4444); // => pageIncrementReducer returned undefined
this.props.listPop();
}
render(){
return (
<Layout>
<LandingBanner/>
<LandingNavgrid/>
<LandingApp/>
</Layout>
)
}
}
const filterNumbers = (list=[]) => {
console.log('filterNumbers list: ', list);
return list.filter(listElement => !!Number(listElement));
};
const mapStateToProps = (state, ownProps) => {
return {
someNumber: state.someNumber,
someList: filterNumbers(state.someList),
navigationCount: state.navigationCount,
};
};
const mapDispatchToProps = (dispatch) => {
return {
pageIncrement: () => dispatch({ type: PAGE_INCREMENT }),
numberIncrease: () => dispatch({ type: NUMBER_INCREASE }),
numberDecrease: () => dispatch({ type: NUMBER_DECREASE }),
listPush: () => dispatch({ type: LIST_PUSH }),
listPop: () => dispatch({ type: LIST_POP }),
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(LandingPage);
Errors
redux.js:449 Uncaught Error: Given action "LIST_POP", reducer
"pageIncrementReducer" 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.
first of all, you always need to return state on the default switch case.
default:
return state;
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
})
In my component I want to check when the parameter has changed and update accordingly. However when I do this, I am seeing weird behaviour and multiple requests been made to my api.
my component:
componentWillMount() {
this.state = {
path: this.props.match.params.categoryName,
};
}
componentDidUpdate(prevProps) {
if (prevProps === undefined) {
return false;
}
if (this.state.path !== this.props.match.params.categoryName) {
this.getCategory()
}
}
getCategory() {
if (this.props.allPosts && this.props.allPosts.length) {
const categoryId = _.result(_.find(this.props.allPosts, v => (
v.name === this.props.match.params.categoryName ? v.id : null
)), 'id');
this.props.dispatch(Actions.fetchCategory(categoryId));
}
}
my action:
import Request from 'superagent';
import Constants from '../constants';
const Actions = {
fetchCategory: categoryId => (dispatch) => {
dispatch({ type: Constants.CATEGORY_FETCHING });
Request.get(`/api/v1/categories/${categoryId}`)
.then((data) => {
dispatch({
type: Constants.CATEGORY_RECEIVED,
category: { id: data.body.id, name: data.body.name },
category_posts: data.body.posts,
});
});
},
};
export default Actions;
my reducer:
import Constants from '../constants';
const initialState = {
posts: [],
category: [],
fetching: true,
};
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
case Constants.CATEGORY_FETCHING:
return Object.assign({}, state, { fetching: true });
case Constants.CATEGORY_RECEIVED:
return Object.assign({}, state, { category: action.category,
posts: action.category_posts,
fetching: false });
default:
return state;
}
}