I'm learning redux with typescript.
Until I hit combineReducer,
export const reducerBase= combineReducers({
stateA: reducerA,
stateB: reducerB,
stateC: reducerC
});
is working fine, but I can't manage to hand write it.
currently what I'm trying to do is
function initialState(): BaseState{
return {
a: [],
b: [],
c: []
};
}
type AllActions = AAction | BAction | CAction;
// tslint:disable-next-line:no-any
function isAAction(arg: any) {
return arg.includes('_A');
}
export function reducerBase(
state: BaseState= initialState(),
action: AllActions
): BaseState{
if (isAAction(action.type)) {
return { ...state, a: reducerA(state.a, action) }; // here the problem
}
return state;
}
type AllActions is not assignable to type AAction,
How should I proceed?
It's working,
export function reducerBase(
state: BaseState= initialState(),
action: AllActions
): BaseState{
a: reducerA(state.a, action as AAction);
b: reducerB(state.b, action as BAction);
c: reducerC(state.c, action as CAction);
return {a,b,c};
}
while BAction is called on reducerA and reducerC, they do nothing and just returned their own state.
The problem is because isAAction function just returns boolean, there is no way Typescript could know that this boolean indicates action type.
First solution - just cast it to AAction:
return { ...state, a: reducerA(state.a, action as AAction) };
Second solution - change isAAction method into typeguard, then Typescript will know that this function returns true when AAction is passed as argument
function isAAction(action: any): arg is AAction {
return action.type.includes('_A');
}
// ...
if (isAAction(action)) {
return { ...state, a: reducerA(state.a, action) };
}
EDIT: Reffering to your comment. I personally wouldn't use reducerBase reducer at all and just used the following structure. Action type will determine which reducer should react on given action.
const reducers = {
stateA: reducerA,
stateB: reducerB,
stateC: reducerC
};
export const store = createStore(
combineReducers(reducers)
);
export function reducerA(state = new StateA(), action: AAction) {
switch (action.type) {
case AActionType.SOME_ACTION: return {
...state,
// some changes based on action
}
case AActionType.OTHER_ACTION: return {
...state,
// some changes based on action
}
default: return state;
}
}
Related
I have only been using TypeScript a couple months, and I just noticed that the compiler is not enforcing the shape of data a function accepts if that function is accessed through React.useContext().
This setup here is not exactly what I have going on, but it more or less shows the problem I am trying to figure out.
import * as React from 'react';
//==>> Reducer State Interface
interface InitialStateInterface {
handleSettingFooBar: Function;
foo: string | null;
bar: boolean;
}
//==>> Function Interface
interface PayloadInterface {
foo?: string | null;
bar?: boolean;
}
//==> Reducer
interface ReducerInterface {
type: string;
payload: PayloadInterface;
}
enum ActionTypes {
SET_FOO = 'SET_FOO',
SET_BAR = 'SET_BAR',
}
const initialState: InitialStateInterface = {
handleSettingFooBar: () => null,
foo: null,
bar: false,
};
const SomeContext = React.createContext<InitialStateInterface>(initialState);
const ContextReducer = (state: any, { type, payload }: ReducerInterface) => {
switch (type) {
case ActionTypes.SET_FOO:
return { ...state, foo: payload.foo };
case ActionTypes.SET_BAR:
return { ...state, bar: payload.bar };
default:
return state;
}
};
const SomeProvider = () => {
const [state, dispatch] = React.useReducer(ContextReducer, initialState);
function handleSettingFooBar(data: PayloadInterface) {
let { foo, bar } = data;
if (foo) dispatch({ type: ActionTypes.SET_FOO, payload: { foo } });
if (bar) dispatch({ type: ActionTypes.SET_BAR, payload: { bar } });
}
/** Okay, of course, no error */
handleSettingFooBar({ foo: 'test' });
/** Error as expected, type does not match */
handleSettingFooBar({ foo: false });
/** Error as expected, key does not exist */
handleSettingFooBar({ randomKey: 'cant do this' });
return <SomeContext.Provider value={{ ...state, handleSettingFooBar }} />;
};
/* ===> But when writing a component that uses that context <=== */
export const SomeComponent = () => {
const { handleSettingFooBar } = React.useContext(SomeContext);
/** Why is the compiler not yelling at me here??? */
handleSettingFooBar({ randomKey: 'hahaha' });
};
export { SomeProvider, SomeContext };
I have tried putting the interface in when calling the context, like this:
const { handleSettingFooBar } = React.useContext<InitialStateInterface>(SomeContext);
But that made no difference.
I am expecting that if somebody is authoring a component that uses this context and its provided functions, that it will regulate the data (at compile time, of course) they try to pass in so a generic setter may not add a value that does not belong in the context reducer state.
Please help, thanks!
The SomeContext has the InitialStateInterface type which defines handleSettingFooBar as handleSettingFooBar: Function, and it does not know how you actually implemented it.
You can change that to handleSettingFooBar: (data:PayloadInterface) => void and then the typescript would know what kind of input should be allowed for it.
I have the following redux action type in my react-ts project
type DataItem = {
id: string
country: string
population: number
}
type DataAction = {
type: string,
payload?: DataItem
}
I have optional payload property, because sometimes I don't pass a payload to the reducer. However, because of this, inside the reducer, typescript complains that payload may be undefined, although I don't use payload in the reducer unless I pass it in the action. Currently what I do I put ! after the payload payload! inside the reducer, but I wonder if there is another way to handle this issue, like maybe create two action types, one with a payload, and one without?
You can use a conditional to check if the payload exists:
TS Playground
type DataAction = {
type: 'example';
payload?: DataItem;
};
function reduce (state: unknown, action: DataAction) {
switch (action.type) {
case 'example': {
if (!action.payload) return state;
action.payload // now, action.payload is DataItem
// ...whatever you need to do with payload
}
}
}
Or use separate action types to handle the cases individually:
TS Playground
type DataActionWithoutPaylod = {
type: 'no-payload';
};
type DataActionWithPayload = {
type: 'with-payload';
payload: DataItem;
};
type DataAction = DataActionWithPayload | DataActionWithoutPaylod;
function reduce (state: unknown, action: DataAction) {
switch (action.type) {
case 'no-payload': {
return state;
}
case 'with-payload': {
action.payload // now, action.payload is DataItem
// ...whatever you need to do with payload
}
}
}
I am currently making a sample project in AngularJs combined with Redux.
I am struggling to get the mappings from the reducer working.
I have a simple input where users can set a new name together with a drop down to select a 'company'.
<input type="text" ng-model="$ctrl.single.object.name">
<select ng-change="$ctrl.getProperties()"
ng-options="option.description as option.description for option in $ctrl.list.all"
ng-model="$ctrl.single.object.company">
When the user changes the company, new properties need to be fetched in order for the user to set these properties.
function FooController($ngRedux, FooActions, BarActions) {
this.$onInit = function () {
this.unsubscribeCompanies = $ngRedux.connect(this.mapStateToThis, BarActions)(this);
this.fetchCompanyList();
};
this.$onDestroy = function () {
this.unsubscribeCompanies();
};
this.fetchCompanyList = function () {
this.fetchCompanies().payload.then((response) => {
this.fetchCompaniesSuccess(response.data);
}, (error) => {
this.fetchCompaniesError(error.data);
});
};
this.getProperties = function () {
this.fetchCompanyProperties(this.single.object.company).payload.then((response) => {
this.fetchCompanyPropertiesSuccess(response.data);
}, (error) => {
this.fetchCompanyPropertiesError(error.data);
});
};
this.mapStateToThis = function (state) {
return {
list: state.bar.list,
single: state.bar.single
};
};
}
module.exports = {
template: require('./index.html'),
controller: ['$ngRedux', 'FooActions', 'BarActions', FooController]
}
The problem I get is that the name and the selected company are overwritten with empty values when the fetch for properties is successful. I get why the values are overwritten with empty values and I have found a way to get it working.
export const GET_COMPANIES = 'GET_COMPANIES';
export const GET_COMPANIES_SUCCESS = 'GET_COMPANIES_SUCCESS';
export const GET_COMPANIES_ERROR = 'GET_COMPANIES_ERROR';
export const GET_COMPANIES_PROPERTIES = 'GET_COMPANIES_PROPERTIES';
export const GET_COMPANIES_PROPERTIES_SUCCESS = 'GET_COMPANIES_PROPERTIES_SUCCESS';
export const GET_COMPANIES_PROPERTIES_ERROR = 'GET_COMPANIES_PROPERTIES_ERROR';
export default function BarActions($http) {
function fetchCompanies() {
return {
type: GET_COMPANIES,
payload: $http.get('api/companies')
};
}
function fetchCompaniesSuccess(companies) {
return {
type: GET_COMPANIES_SUCCESS,
payload: companies
};
}
function fetchCompaniesError(error) {
return {
type: GET_COMPANIES_ERROR,
payload: error
};
}
function fetchCompanyProperties(company) {
return {
type: GET_COMPANIES_PROPERTIES,
payload: $http.get(`api/company/${company}/properties`)
};
}
function fetchCompanyPropertiesSuccess(properties) {
return {
type: GET_COMPANIES_PROPERTIES_SUCCESS,
payload: properties
};
}
function fetchCompanyPropertiesError(error) {
return {
type: GET_COMPANIES_PROPERTIES_ERROR,
payload: error
};
}
return {
fetchCompanies,
fetchCompaniesSuccess,
fetchCompaniesError,
fetchCompanyProperties,
fetchCompanyPropertiesSuccess,
fetchCompanyPropertiesError
}
}
The way I overwrite the values in the reducer is as follows:
import { GET_COMPANIES, GET_COMPANIES_SUCCESS, GET_COMPANIES_ERROR, GET_COMPANIES_PROPERTIES, GET_COMPANIES_PROPERTIES_ERROR, GET_COMPANIES_PROPERTIES_SUCCESS } from "../actions/bar.actions";
const all = [];
const initialState = {
list: {
all,
filtered: all,
error: null,
loading: false
},
single: {
object: {},
error: null,
loading: false
}
};
export function BarReducer(state = initialState, action) {
switch (action.type) {
case GET_COMPANIES:
return { ...state, list: { all: [], filtered: [], error: null, loading: true } };
case GET_COMPANIES_SUCCESS:
return { ...state, list: { all: action.payload, filtered: action.payload, error: null, loading: false } };
case GET_COMPANIES_ERROR:
return { ...state, list: { all: [], filtered: [], error: action.payload.innerException, loading: false } };
case GET_COMPANIES_PROPERTIES:
return { ...state, single: { ...state.single, object: { ...state.single.object }, error: null, loading: true } };
case GET_COMPANIES_PROPERTIES_SUCCESS:
return { ...state, single: { ...state.single, object: { ...state.single.object, payloadValues: action.payload }, error: null, loading: false } };
case GET_COMPANIES_PROPERTIES_ERROR:
return { ...state, single: { object: null, error: action.payload.innerException, loading: false } };
default:
return state;
}
}
The way I now use the spread operator in order to overwrite the old state feels dirty. I was wondering if there are any rules or guidelines to handle this issue. So far I have searched a while on internet and in specific the Redux website but I did not come cross any other solutions.
The breakage is likely due to the structure of the reducer. It is concerned with too many different parts of state and has to operate on deep nested objects, making it easy to accidentally mutate state. The guidelines for reducer structure say that splitting reducer state into normalized slices is the best way to go.
Try splitting your one reducer into multiple smaller reducers. For example:
export const all = (initialAll = [], { type, companies }) => {
switch(type) {
case GET_COMPANIES_SUCCESS: return companies;
default: return initialAll;
}
}
export const error = (initialError = '', { type, error }) => {
switch(type) {
case GET_COMPANIES_ERROR: return error;
default: return initialError;
}
}
export const isFetching = (isFetching = false, { type }) => {
switch(type) {
case GET_COMPANIES: return true;
case GET_COMPANIES_SUCCESS: return false;
case GET_COMPANIES_ERROR: return false;
default: return isFetching;
}
}
Then, compose them into one reducer:
import { combineReducers } from 'redux';
export list = combineReducers({
all,
error,
isFetching
});
// ...
export rootReducer = combineReducers({
list,
single,
// ...
})
This way, each reducer is concerned with only one thing or set of things, and its reduction handlers can do simple operations on single-level state instead of complex operations on deep nested state.
Also, in your list substate, it looks like you are storing the same type of collection resources in both all and filtered with potential overlap. This leads to multiple sources of truth for the same data, which opens the door to data inconsistency. Instead, keep an array of filteredIds:
export const filteredIds = (initialIds = [], { type, filteredIds }) => {
switch(type) {
case SET_FILTERED_IDS: return filteredIds;
default: return initialIds;
}
}
Then, use a selector that filters all by the filteredIds to get your filtered items.
One option is to use Immutable, which would change your reducers to:
case GET_COMPANIES:
return state.setIn(['list', 'loading'], true);
// etc
See Using Immutable.JS with Redux for more information about this approach.
Another option is to use Lodash, as shown in this Issue, you can define the following function to make it similar to the immutable one:
import {clone, setWith, curry} from 'lodash/fp';
export const setIn = curry((path, value, obj) =>
setWith(clone, path, value, clone(obj)),
);
Then you can use setIn as follow:
case GET_COMPANIES:
return setIn(['list', 'loading'], true, state);
// etc
The Lodash approach is just working with plain object, so it might be easier to understand than Immutable.
My store is the next:
const initialState = {
all: {
"name": "",
"datas": {
"data1": {
"subdata1": [],
"subdata2": [],
},
"data2": {
"subdata1": [],
"subdata2": [],
},
}
}
}
I have the next Components:
* All: main Component, map to Data with all.datas
* Data: show data1 or data2 or data3. map to Subdata with data1 and data2 ...
* Subdata: show list subdata1
I have a redux connect in Subdata, and I want that the reducers, instead of return general state, return a subdata state.
For example:
Subdata.js:
class Subdata extends React.Component {
// ....
}
function mapStateToProps(state, ownProps) {
return {
subdata: ownProps.subdata
};
}
function mapDispatchToProps(dispatch) {
return { actions: bindActionCreators(Actions, dispatch) }
}
export default connect(mapStateToProps, mapDispatchToProps)(Subdata);
reducers.js:
const subdata = (state = {}, action) => {
// I want that state return: subdata, in this case, []
// instead of all: {...}
const copy = JSON.parse(JSON.stringify(state));
switch(action.type) {
case "ADD_SUBDATA":
// In this moment: I need pass a lot of vars: data_id and subdata_id
my_subdata = state.datas[data_id][subdata_id]
// I want get directly the subdata
my_subdata = state.subdata
default:
return state;
}
}
Is this possible? How?
Thanks you very much.
Yes it is possible.
You could consider using dot-prop-immutable or Immutable.js in your reducer to selectively operate on a "slice" of your store.
Pseudo code using dotProp example:
// Getter
dotProp.get({foo: {bar: 'unicorn'}}, 'foo.bar')
Signature:
get(object, path) --> value
I notice in your code you are using the following to "clone" your state:
const copy = JSON.parse(JSON.stringify(state));
This is not optimal as you are cloning completely all your state object, when instead you should operate only on a part of it (where the reducer operates, specially if you are working with combined reducers).
I want to separate a reducer into N more combining together 1 key.
Say I have an initial state:
const STATE_INITIAL = {
nested_component: {
field1: 1,
field2: 2
},
upper_field: 3
}
Then I have a reducer:
function reducer(state=STATE_INITIAL, action){
switch(action){
case ACTION_UPPER_FIELD:
return ...
case ACTION_GRID1:
return ...
case ACTION_GRID2:
return ...
default:
return state;
}
}
Why I want to do it?
I want to have a component that I can reuse throughout the project. It would always come with its initial state and would have its reducer that I'd like to connect to the rest of the application.
My solution
One way I can think of is stacking cases for grid actions, providing it with state.gridand its own initial state and combing the result with the state:
const STATE_INITIAL = {
nested_component: {},
upper_field: 3
};
function reducer(state=STATE_INITIAL, action){
switch(action){
case ACTION_UPPER_FIELD:
return ...
case ACTION_GRID1:
case ACTION_GRID2:
return reducerGrid(state.grid, action);
default:
return state;
}
}
const STATE_INITIAL_GRID = {
field1: 1,
field2: 2
};
function reducerGrid(state = STATE_INITIAL_GRID, action) {
switch(action){
case ACTION_GRID1:
return ...
case ACTION_GRID2:
return ...
default:
return state;
}
}
Is there a standardized approach or is my solution fine? The things I don't like about it is the default in the reducerGrid seems redundant now and I am also not satisfied with having to repeat the actions in both reducers.
My 2nd Solution
function reducer(state=STATE_INITIAL, action){
const stateGrid = reducerGrid(state.grid, action)
let stateNew = state;
if(stateGrid !== state.grid){
stateNew = {...state, grid: ...stateGrid}
}
switch(action){
case ACTION_UPPER_FIELD:
return {...stateNew, ... };
default:
return stateNew;
}
}
3rd Solution
function reducer(state=STATE_INITIAL, action){
const stateNew = {...state, grid: ...reducerGrid(state.grid, action)};
switch(action){
case ACTION_UPPER_FIELD:
return ...
default:
return stateNew;
}
}
I have finally found a solution I am satisfied with.
Using this method:
import R from 'ramda';
function splitReducers(reducers, rest) {
return (state, action) => {
const reducersPrepared = R.mapObjIndexed((reducer, key) => {
return reducer(R.defaultTo({}, state)[key], action);
})(reducers);
const getUndefinedIfEmpty = R.ifElse(
R.isEmpty,
() => undefined,
R.identity
);
const stateWithoutSplitKeys = R.pipe(
R.omit(R.keys(reducers)),
getUndefinedIfEmpty
)(state);
return R.merge(
reducersPrepared,
rest(stateWithoutSplitKeys, action)
);
}
}
I can write my state tree in the following way:
Ports: splitReducers({
grid: reducerGrid,
}, reducer);
This will result in the object with keys split:
{
Ports: {
grid: {...},
isSaving: true,
isVisible: false
}
}
After applying the method the root-reducer is showing more of its state at the first glance:
export const rootReducer = combineReducers({
pageAllocation: combineReducers({
tabNetwork: combineReducers({
popupNetworkTemplates: reducerPopupNetworkTemplates,
gridPorts: splitReducers({ // <----- HERE IT IS
grid: reducerGridPortsOnly
}, reducerPorts),
}),
tabStorage: () => ({}),
activeTab: reducerPortsActiveTab
}),