Allow partial type - redux

Using Flowtype together with Redux, I have a type like this:
export type MapState = {
addresses: Address[],
selected: Array<number>
}
and an action creator:
export const setParams = (params: any): Action => {
return { type: actionTypes.SET_PARAMS, payload: { params };
}
In the reducer, I merge the params into the state:
export default (state: MapState = initialState, action: SetParamsAction) => {
switch (action.type) {
case actionTypes.SET_PARAMS: {
return {
...state,
...action.payload.params
}
[...]
I'm looking for a possibility to tell Flowtype to accept params in the action creator, if it is an object consisting only of properties of MapState, so that I can get rid of the any in setParams. Any idea?

You can just add a exact PossibleParams Object type like so:
type PossibleParams = {|
addresses?: Address[],
selected?: number[],
|};
export const setParams = (params: PossibleParams): Action => ({
type: actionTypes.SET_PARAMS,
payload: {
params,
},
});
You can check all the possibilities on flow.org/try 🙂

Related

extraReducers in createSlice() in Redux Toolkit

Here's the example code from Codecademy:
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
import { client } from '../api';
const initialState = {
todos: [],
status: 'idle'
};
export const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
const response = await client.get('/todosApi/todos');
return response.todos;
});
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
addTodo: (state, action) => {
state.todos.push(action.payload);
}
},
extraReducers: {
[fetchTodos.pending]: (state, action) => {
state.status = 'loading';
},
[fetchTodos.fulfilled]: (state, action) => {
state.status = 'succeeded';
state.todos = state.todos.concat(action.payload);
}
}
});
I'm confused about what fetchTodos.pending and fetchTodos.fulfilled mean as computed properties. I don't see that fetchTodos has those attributes. What is going on?
Those are generated by createAsyncThunk
Check out the RDK docs on createAsyncThunk
Parameters#
createAsyncThunk accepts three parameters: a string action type value, a payloadCreator callback, and an options object.
type
A string that will be used to generate additional Redux action type
constants, representing the lifecycle of an async request:
For example, a type argument of 'users/requestStatus' will generate these
action types:
pending: 'users/requestStatus/pending'
fulfilled: 'users/requestStatus/fulfilled'
rejected: 'users/requestStatus/rejected'

Do actions added with extraReducers on createSlice have the slice's name prefix added to their types?

From the official doc's example:
https://redux-toolkit.js.org/api/createSlice#the-extrareducers-builder-callback-notation
import { createAction, createSlice } from '#reduxjs/toolkit'
const incrementBy = createAction<number>('incrementBy')
const decrement = createAction('decrement')
createSlice({
name: 'counter',
initialState: 0,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(incrementBy, (state, action) => { // DO SOMETHING })
.addCase(decrement, (state, action) => { // DO SOMETHING })
.addDefaultCase((state, action) => {})
},
})
Also from the docs:
One of the key concepts of Redux is that each slice reducer "owns" its slice of state, and that many slice reducers can independently respond to the same action type. extraReducers allows createSlice to respond to other action types besides the types it has generated.
QUESTION
In the example above, will the cases incrementBy and decrement also get the counter name as a prefix in their types?
Like:
"counter/incrementBy"
"counter/decrement"
Is this how the extraReducers property work?
No, because the entire point of extraReducers is that it does not generate any new action types.
extraReducers exists so that a slice reducer can listen to other action types that have already been defined outside the slice.
No. It does not get the name prefix.
https://codesandbox.io/s/xenodochial-dew-35ivq
import { createAction, createSlice } from "#reduxjs/toolkit";
interface CounterState {
value: number;
}
export const decrementV2 = createAction('decrement');
const initialState = { value: 0 } as CounterState;
const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment(state,action) {
console.log(`action.type: ${action.type}`);
state.value++;
},
decrement(state,action) {
console.log(`action.type: ${action.type}`);
state.value--;
}
},
extraReducers: (builder) => {
builder.addCase(decrementV2, (state, action) => {
console.log("FROM decrementV2 (from extraReducers)")
console.log(`action.type: ${action.type}`);
state.value--;
});
}
});
export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;

Getting Flow warning messages with Reducer

Using the React Context API, I've built this reducer:
export const usersReducer = (state: UsersState, action: UsersAction) => {
switch (action.type) {
case TOGGLE_MODAL: {
return {
...state,
isModalOpen: !state.isModalOpen
};
}
case CANCEL_REQUEST: {
return {
...state,
isCancelRequest: action.payload
};
}
case UPDATE_COMPANY: {
return {
...state,
companyId: action.payload
};
}
default: {
return state;
}
}
}
The associated Actions look like this:
// Note: `ActionType` = `string`
export const TOGGLE_MODAL: ActionType = 'TOGGLE_MODAL';
export const CANCEL_REQUEST: ActionType = 'CANCEL_REQUEST';
export const UPDATE_COMPANY: ActionType = 'UPDATE_COMPANY';
type ToggleModalAction = {type: typeof TOGGLE_MODAL};
type CancelRequestAction = {type: typeof CANCEL_REQUEST, payload: boolean};
type UpdateCompanyAction = {type: typeof UPDATE_COMPANY, payload: number};
export type UsersAction =
| ToggleModalAction
| CancelRequestAction
| UpdateCompanyAction;
On the two action.payload instances, Flow is saying this:
Cannot get `action.payload` because property `payload` is missing in `ToggleModalAction`
I thought the way I defined my 3 "...Action" types, I could include payload where warranted and exclude it where it's not needed, like in ToggleModalAction.
Any ideas how to solve this?
By doing typeof TOGGLE_MODAL, etc., the type key of your UsersAction type will always be string. What you need in order to get type help from Flow's disjoint unions is the string literals themselves:
type ToggleModalAction = {type: 'TOGGLE_MODAL'};
type CancelRequestAction = {type: 'CANCEL_REQUEST', payload: boolean};
type UpdateCompanyAction = {type: 'UPDATE_COMPANY', payload: number};

Redux action not dispatched. TypeError: Invalid attempt to spread non-iterable instance

In my application I want to add a 'ticket' to an array in the 'event' object. In the action I post the new ticket to the database, and after that I dispatch the action to the reducer. By using the Redux logger, I am able to retrieve the error:
The action of 'createTicket' is this:
// actions/tickets.js
export const TICKET_CREATE_SUCCESS = 'TICKET_CREATE_SUCCESS';
const ticketCreateSuccess = tickets => ({
type: TICKET_CREATE_SUCCESS,
tickets
});
export const createTicket = (eventId, data) => (dispatch, getState) => {
const jwt = getState().currentUser.token;
const id = getState().currentUser.userId;
const email = getState().currentUser.email;
const name = getState().currentUser.name;
request
.post(`${baseUrl}/events/${eventId}/tickets`)
.set('Authorization', `Bearer ${jwt}`)
.send(data)
.then(response => {
dispatch(ticketCreateSuccess({ ...response.body, user: { id, email, name } }));
})
.catch(error => error);
};
The reducer
// reducers/events.js
import { EVENT_FETCHED } from '../actions/events';
import { TICKET_EDIT_SUCCESS, TICKET_CREATE_SUCCESS } from '../actions/tickets';
export default (state = null, action = {}) => {
switch (action.type) {
case EVENT_FETCHED:
return action.event;
case TICKET_EDIT_SUCCESS:
return {
...state,
tickets: state.tickets.map(ticket => {
if (ticket.id === action.ticket.id) {
return action.ticket;
}
return ticket;
})
};
case TICKET_CREATE_SUCCESS:
console.log({ ...state, tickets: [...state.tickets, action.tickets] });
return { ...state, tickets: [...state.tickets, action.tickets] };
default:
return state;
}
};
The reducers are combined into :
import { combineReducers } from 'redux';
import currentUser from './currentUser';
import events from './events';
import event from './event';
import ticket from './ticket';
import tickets from './tickets';
import numberOfTickets from './numberOfTickets';
export default combineReducers({ currentUser, events, event, ticket, tickets, numberOfTickets });
Could it be that you're trying to spread your reducer state when its value is null:
export default (state = null, action = {}) => {
return {
...state, // Here
// rest
}
Your default state should probably be an object, e.g.:
const InitialState = {
tickets: []
};
export default (state = InitialState, action) => {
// Some code
case TICKET_CREATE_SUCCESS:
return {
...state,
tickets: [
...state.tickets,
action.tickets
]
}
}
just add this ...state || []
and you are good to go.
the problem is value of ...state equals null with empty array and when you try to iterate over null it creates an error.
so use and "OR" operator and it will work fine.

State does not change in my reducer when action is dispatched

I am not able to retrieve the state in the reducer
MyComponent looks like this
const MyComponent = ({name, features, onClick}) => {
return (
<div>
Hello! {name}
<Button onClick={() => { onClick(features); }}> Weight</Button>
</div>
);
const mapDispatchToProps = (dispatch: any) => {
return {
onClick: (features) => {
dispatch(weightSort(features));
}
};
};
const mapStateToProps = (state: any, ownProps: any) => {
console.log(state); //Displays the state
return {
name: "John Doe",
features: ownProps.features,
};
};
export const FeatureBlock = connect(mapStateToProps, mapDispatchToProps)(MyComponent);
My actions and reducers looks like below:
// Action Creator
export const weightSort = (features) => {
console.log("inside the weight sort action creator!!!");
return {
type: "SET_WEIGHT_FILTER",
filter: "DESC",
features,
};
};
// Reducer
export const weightFilter = (state = [], action) => {
switch (action.type) {
case "SET_WEIGHT_FILTER":
console.log(state); // Gives me empty state
console.log("++inside weight filter+++++", action); //Displays action
return state;
default:
return state;
}
};
export const FeatureBlock = connect(
mapStateToProps,
mapDispatchToProps,
)(MyComponent);
What am I missing here? Any help will be appreciated!
In your reducer, when you console.log(state), it is correct in returning an empty array because you haven't done anything to modify it.
// Reducer
export const weightFilter = (state = [1,2,3], action) => {
switch (action.type) {
case "SET_WEIGHT_FILTER":
console.log(state); // This will show [1,2,3] because [1,2,3] is the initial state.
console.log("++inside weight filter+++++", action); //Displays action
return state;
default:
return state;
}
};
My guess is that you want something like this for your reducer:
// Action Creator
export const weightSort = (name, features) => {
console.log("inside the weight sort action creator!!!");
return {
type: "SET_WEIGHT_FILTER",
name,
features,
};
};
// Reducer
export const weightFilter = (
state = {
name: '',
features: [],
},
action
) => {
switch (action.type) {
case "SET_WEIGHT_FILTER":
return {...state, name: action.name, features: action.features}
default:
return state;
}
};
and then in your mapStateToProps you would map out the attributes like so:
const mapStateToProps = (state: any, ownProps: any) => {
console.log(state); //Displays the state
return {
name: state.weightFilter.name,
features: state.weightFilter.features,
};
};
and your button would have a name prop passed into the function like so:
<Button onClick={() => { onClick(name, features); }}> Weight</Button>
If you would like to sort your data, you can do so either in the reducer or inside the container. I prefer to do it in the container and like to use the lodash sortBy function. It works like this:
import { sortBy } from 'lodash' //be sure to npm install lodash if you use this utility
...
...
function mapStateToProps(state) {
return {
name: state.weightFilter.name,
features: sortBy(features, ['nameOfPropertyToSortBy'])
};
}
Here is the lodash documentation on sortBy: https://lodash.com/docs/4.17.4#sortBy
Hope that helps!

Resources