Discriminated/disjoint unions in Flow - flowtype

I'm trying to make Flow happy.
It's not very, giving me this:
app/components/commands/types.js:117
117: { state: 'state-retries-timeout',
^^^^^^^^^^^^^^^^^^^^^^^ string literal `state-retries-timeout`. Expected string literal `state-initial`, got `state-retries-timeout` instead
60: { state: 'state-initial', touched: boolean }
But I don't see how I'm not following the documentation
Source code:
// #flow
/**
* A server-side validation message. Should contain a globally unique key
* and a message. The key can be used for internationalisation purposes.
*/
export type ValidationMessage =
{ key: string,
message: string }
export function validationMessage(key: string, message: string): ValidationMessage {
return { key, message }
}
/**
* The field is untouched in this user-session.
*/
export const StateInitial = 'state-initial';
/**
* The edit is not yet persisted anywhere.
*/
export const StatePending = 'state-pending'
/**
* The command is not yet committed on the server, but is committed locally.
*/
export const StatePendingSavedLocally = 'state-pending-saved-locally'
/**
* The command was successfully commited.
*/
export const StateSuccess = 'state-success'
/**
* The command was commited, but there are resulting warnings.
*/
export const StateSuccessWarnings = 'state-success-warnings'
/**
* The command or its data was invalid and the server returned 400 Bad Request;
* it may not be retried without changing it.
*/
export const StateRejected = 'state-rejected'
/**
* Despite numerous retries, the app failed to persist the change to the server.
*/
export const StateRetriesTimeout = 'state-retries-timeout'
export type Initial =
{ state: 'state-initial',
touched: boolean }
export type Pending =
{ state: 'state-pending',
touched: boolean }
export type PendingSavedLocally =
{ state: 'state-pending-saved-locally',
touched: boolean }
export type Success =
{ state: 'state-success',
touched: boolean }
export type SuccessWarnings =
{ state: 'state-success-warnings',
warnings: ValidationMessage[],
touched: boolean }
export type Rejected =
{ state: 'state-rejected',
errors: ValidationMessage[],
touched: boolean }
export type RetriesTimeout =
{ state: 'state-retries-timeout',
touched: boolean }
/**
* The discriminated union of all states we allow fields to be represented as.
*/
export type ValueChangeState =
| Initial
| Pending
| PendingSavedLocally
| Success
| SuccessWarnings
| Rejected
| RetriesTimeout
export const initial: ValueChangeState =
{ state: 'state-initial',
touched: false }
export const pending: ValueChangeState =
{ state: 'state-pending',
touched: true }
export const pendingSavedLocally: ValueChangeState =
{ state: 'state-pending-saved-locally',
touched: true }
export const success: ValueChangeState =
{ state: 'state-success',
touched: true }
export function successWarnings(warnings: ValidationMessage[], touched: boolean = true): ValueChangeState {
return {
state: 'state-success-warnings',
warnings,
touched
}
}
export function rejected(errors: ValidationMessage[], touched: boolean = true): ValueChangeState {
return {
state: 'state-rejected',
errors,
touched
}
}
export const retriesTimeout: ValueChangeState =
{ state: 'state-retries-timeout',
touched: true }
Sample usage
// #flow
/*eslint no-unused-expressions: 0 */
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { initial, pending } from './types';
import { valueStateReducer } from './reducers';
import { successOf, rejectionOf, timeoutOf, successWarningsOf } from './actions';
describe('(Reducer) components/commands', function() {
describe('valueStateReducer', function() {
const noop = () => ({ type: 'NOOP' });
const testing = () => ({ type: 'commands/TESTING' });
const subject = valueStateReducer('commands/TESTING');
it('other makes no change', function() {
const res = subject(initial, noop());
expect(res).to.deep.eq(initial);
});
it('of action makes pending', function() {
const res = subject(initial, testing());
expect(res).to.deep.eq(pending);
});
});
});

The answer is that flow doesn't properly handle destructive assignment of nullable/undefined discriminated union/sum types when also providing a default value.
In this screenshot I default valueState to initial (one of the DU cases). This triggers the bug.
If I instead default further down, flow doesn't barf:

Related

NGRX Select returning Store not a value

catalogueSelection in Store Image
I have the data I require in state (catalogueSelection: searchTextResult & categoryCheckboxResult) and need to pass the string 'SearchTextResult' into one component and the Array 'categoryCheckboxResult' into another.
When I try to retrieve the required values I am retrieving the whole store. I have looked at numerous websites and entries here but getting very confused now.
Model:
export class SearchTextResult {
searchTextResult: string;
}
export class CategoryCheckboxResult {
categoryCheckboxResult:Array<CategoryCheckboxResult>;
}
Actions:
import { Action } from '#ngrx/store';
import { SearchTextResult, CategoryCheckboxResult } from 'app/#core/services/products/products.model';
export enum UserCatalogueSelectionTypes {
AddSearchTextResult = '[SearchTextResult] AddResult',
AddCategoryCheckboxResult = '[CategoryCheckboxResult] AddResult',
GetSearchTextResult = '[SearchTextResult] GetResult',
}
export class AddSearchTextResult implements Action {
readonly type = UserCatalogueSelectionTypes.AddSearchTextResult;
constructor(public payload: SearchTextResult){
}
}
export class AddCategoryCheckboxResult implements Action {
readonly type = UserCatalogueSelectionTypes.AddCategoryCheckboxResult;
constructor(public payload: CategoryCheckboxResult){
}
}
export class GetSearchTextResult implements Action {
readonly type = UserCatalogueSelectionTypes.GetSearchTextResult;
}
export type UserCatalogueSelectionUnion =
| AddSearchTextResult
| AddCategoryCheckboxResult
| GetSearchTextResult
Reducers:
import { SearchTextResult, CategoryCheckboxResult} from "app/#core/services/products/products.model";
import { UserCatalogueSelectionTypes, UserCatalogueSelectionUnion} from "../actions/products.actions";
export interface UserCatalogueSelectionState {
searchTextResult: SearchTextResult | null;
categoryCheckboxResult: CategoryCheckboxResult | null;
}
export const initialState: UserCatalogueSelectionState = {
searchTextResult: null,
categoryCheckboxResult: null,
}
export function reducer(state:UserCatalogueSelectionState = initialState, action: UserCatalogueSelectionUnion ): UserCatalogueSelectionState{
switch (action.type) {
case UserCatalogueSelectionTypes.AddSearchTextResult:
return {
...state,
searchTextResult: action.payload,
};
case UserCatalogueSelectionTypes.AddCategoryCheckboxResult:
return {
...state,
categoryCheckboxResult: action.payload,
};
case UserCatalogueSelectionTypes.GetSearchTextResult: {
return state;
}
default: {
return state;
}
}
}
Selectors:
import { createSelector,createFeatureSelector } from "#ngrx/store";
import {UserCatalogueSelectionState} from '../../store/reducer/products.reducer';
export const fetchSearchTextResults = createFeatureSelector<UserCatalogueSelectionState>("searchTextResult");
export const fetchSearchTextResult = createSelector (
fetchSearchTextResults,
(state:UserCatalogueSelectionState) => state.searchTextResult.searchTextResult
);
export const fetchCatalogueCheckBoxResults = createFeatureSelector<UserCatalogueSelectionState>("catalogueCheckboxResult");
export const fetchCatalogueCheckBoxResult = createSelector (
fetchCatalogueCheckBoxResults,
(state: UserCatalogueSelectionState) => state.categoryCheckboxResult.categoryCheckboxResult
);
My Component 1
Observable:
public searchTextResult: Observable<String>;
Contructor: (part of)
private store: Store<fromCatalogueSelection.UserCatalogueSelectionState>
Code Snippet: (asking for the data)
this.searchTextResult = this.store.select('SearchTextResult');
console.log('TESTING SEARCH TEXT: ', this.searchTextResult);
Console:
TESTING SEARCH TEXT: Store {_isScalar: false, actionsObserver: ActionsSubject, reducerManager: >ReducerManager, source: Store, operator: DistinctUntilChangedOperator}
My Component 2
Observable
searchTextResult$: Observable<CatalogueSelectionActions.GetSearchTextResult>;
Code Snippet: (asking for the data)
this.searchTextResult$ = this.store.select('GetSearchTextResult');
console.log('TESTING SEARCH TEXT: ', this.searchTextResult$);
Console:
TESTING SEARCH TEXT: Store {_isScalar: false, actionsObserver: ActionsSubject, reducerManager: > ReducerManager, source: Store, operator: DistinctUntilChangedOperator}
I've given up on the Selectors for the moment. Any help much appreciated as I'm going a round in circles.
You are almost there, the value from the console log is the observable object, everytime you select something from the store, you will get the value wrapped within an observable. You just need to subscribe to it:
this.searchTextResult$ = this.store.select('GetSearchTextResult');
this.searchTextResult$.subscribe((yourData) => console.log(yourData));
Also, since you are working with selectors, use them, you don't have to write the state/selector name:
selector
...
export const fetchCatalogueCheckBoxResult = createSelector (
fetchCatalogueCheckBoxResults,
(state: UserCatalogueSelectionState) =>
state.categoryCheckboxResult.categoryCheckboxResult
);
component
import * as YourSelectors from './store/something/selectors/yourthing.selectors'
...
...
this.searchTextResult$ = this.store
.select(YourSelectors.fetchCatalogueCheckBoxResult)
.subscribe(console.log);
Additionally, try to subscribe using the async pipe delegating that to your template html so you don't have to deal with the subscription in the code, for example:
component
...
export class Component {
searchTextResult$!: Observable<any> // your data type here
...
...
this.searchTextResult$ = this.store
.select(YourSelectors.fetchCatalogueCheckBoxResult)
}
html
<ng-container *ngIf="(searchTextResult$ | async) as result">
<p>Your result value: {{ result }}</p>
</ng-container>

Cannot assign to read only property 'state' of object '#<Object>'

I'm using Redux Toolkit and I'm having trouble in one of my actions. Here's relevant parts of my slice:
export const initialCookingSessionState = {
recipeInfo: null as RecipeInfo | null,
instructions: [] as Instruction[],
ingredients: [] as Ingredient[],
activeTimers: [] as CookingTimer[],
currentStepIndex: 0 as number,
stepTimers: [] as StepTimer[]
};
const cookingSessionSlice = createSlice({
name: 'session',
initialState: initialCookingSessionState,
reducers: {
startRecipe(state, { payload: recipe }: PayloadAction<Recipe>) {
const { info, instructions, ingredients } = recipe;
state.recipeInfo = info;
state.ingredients = [...ingredients];
state.instructions = [...instructions]
state.stepTimers = [];
state.instructions.forEach(({ timers }, stepIndex) => {
timers.forEach(timer =>
state.stepTimers.push({ ...timer, stepIndex, state: CookingTimerState.Pending })
)
})
},
incStep(state) { state.currentStepIndex++ },
decStep(state) { state.currentStepIndex-- },
startTimer(state, { payload: timer }: PayloadAction<StepTimer>) {
timer.state = CookingTimerState.Running
},
}
});
When I dispatch startTimer, I get the error:
Cannot assign to read only property 'state' of object '#'
There must be something about what is and isn't possible with Redux Toolkit's "Mutative State Changes" that I'm missing. It seems to me that my example isn't that different from theirs in the docs, but apparently I'm wrong about that. (the other actions work fine)
In case it's helpful, here are the models, which I think are pretty simple:
export class Recipe {
info: RecipeInfo = {
id: "",
title: ""
};
instructions: Instruction[] = [];
ingredients: Ingredient[] = []
}
export class Instruction {
timers: CookingTimer[] = [];
constructor(public text: string) {}
}
export class Ingredient {
id: string = "";
state: IngredientState = { done: false };
constructor(public text: string) {}
}
export class CookingTimer {
constructor(
public durationSec = 0,
public label = "") {}
}
export enum CookingTimerState {
Pending, Paused, Running, Done
}
export type StepTimer = {
state: CookingTimerState
durationSec: number
label: string
stepIndex: number
}

ngrx/store - 'throw error as Cannot read property'

I am implementing the createFeatureSelector and createSelector - but getting a error as core.js:15714 ERROR TypeError: Cannot read property 'showProductCode' of undefined
I use "#ngrx/store": "^7.1.0",
But not able to find the issue. here is my code :
import { Product } from "./../product";
import * as fromRoot from "./../../state/app.state";
import { createFeatureSelector, createSelector } from "#ngrx/store";
export interface State extends fromRoot.State {
products:ProductState
}
export interface ProductState {
showProductCode : boolean;
currentProduct : Product;
products:Product[]
}
const initialState:ProductState = {
showProductCode : true,
currentProduct:null,
products:[]
}
const getProductFeatureState = createFeatureSelector<ProductState>("product");
export const getShowProductCode = createSelector(
getProductFeatureState,
state => state.showProductCode
);
export const getCurrentProduct = createSelector(getProductFeatureState, state => state.currentProduct);
export const getProducts = createSelector(getProductFeatureState, state => state.products);
export function reducer(state=initialState, action):ProductState {
switch (action.type) {
case "TOGGLE_PRODUCT_CODE":
return {
...state,
showProductCode : action.payload
}
default:
return state;
}
}
you have a typo here:
const getProductFeatureState = createFeatureSelector<ProductState>("product");
"products" is what you've defined and you're selecting "product"

Redux Sagas Recursion Not Working

I want to find a item and its sub items by item id, and I write the following code, but fetchSubItems() always not working, and throwing exception 'TypeError: Cannot read property 'root' of undefined', anyone can help me?
export function *fetchItem(api, id){
const item = yield call (api.getItem, id)
yield put(Actions.addItem(item))
yield call(fetchSubItems, item)
yield put(Actions.success())
}
export function *fetchSubItems(api, item){
if(item.children){
const children = yield item.children.map((id)=>{
return call(api.getItem, id)
})
yield put(Actions.addItems(children))
// the following lines throws 'TypeError: Cannot read property 'root' of undefined'
yield children.map((child)=>{
call(fetchSubItems, api, child)
})
}
}
It seems that a return statement is missing in the last call. The working example:
import Promise from 'bluebird';
import { delay } from 'redux-saga';
import { call } from 'redux-saga/effects';
import {
reducer
} from '../reducers/counter';
import { logger } from '../utils';
const name = '19/Tree_Traversal';
const log = logger(name);
const delayTime = 10;
const tree = {
1: {children: [2, 3]},
2: {children: [4, 5, 6]},
3: {children: []},
4: {children: []},
5: {children: []},
6: {children: [7]},
7: {children: []}
};
const api = {
getItem(id) {
log(`getItem(${id})`);
return delay(delayTime, tree[id]);
}
};
export function *fetchItem(/*api, */id = 1) {
const item = yield call(api.getItem, id);
// yield put(Actions.addItem(item))
yield call(fetchSubItems, /*api, */item);
// yield put(Actions.success())
}
export function *fetchSubItems(/*api, */item) {
if (item.children) {
const children = yield item.children.map((id) => {
return call(api.getItem, id);
});
// yield put(Actions.addItems(children))
yield children.map((child) => {
return call(fetchSubItems, child); // <=== added `return`
});
}
}
export default {
name,
saga: fetchItem,
reducer: reducer,
useThunk: !true,
execute(store) {
return Promise.delay(8 * delayTime)
.then(() => this);
}
};
returns the following log:
00000000: [counter reducer] action Object {type: "##redux/INIT"}
00000003: [Runner] ---------- running example 19/Tree_Traversal
00000004: [Runner] store initial state 0
00000008: [19/Tree_Traversal] getItem(1)
* 00000060: [19/Tree_Traversal] getItem(2)
00000061: [19/Tree_Traversal] getItem(3)
* 00000074: [19/Tree_Traversal] getItem(4)
00000074: [19/Tree_Traversal] getItem(5)
00000075: [19/Tree_Traversal] getItem(6)
* 00000088: [19/Tree_Traversal] getItem(7)
00000091: [Runner] store final state 0
00000092: [Runner] ---------- example 19/Tree_Traversal is done

Redux state and component property undefined until ajax resolves

My component get some properties via props with the function:
const mapStateToProps = state => {
const { entities: { keywords } } = state
const {locale} = state
return {
keywords: keywords[locale]
}
}
I got state keywords using ajax, in the same component:
componentDidMount() {
this.props.loadKeywords()
}
My component gets rendered twice. First, before the ajax resolves, so in my render method I got undefined:
render() {
const { keywords } = this.props.keywords
...
Which is the proper way to solve it? I changed componentDidMount to componentWillMount without success.
Right now, based on the real-world example, I have initialized keywords state with an empty object:
function entities(state = { users: {}, repos: {}, keywords: {} }, action) {
if (action.response && action.response.entities) {
return merge({}, state, action.response.entities)
}
return state
}
My reducer:
import { combineReducers } from 'redux'
import { routerReducer as router } from 'react-router-redux'
import merge from 'lodash/merge'
import locale from './modules/locale'
import errorMessage from './modules/error'
import searchText from './modules/searchText'
// Updates an entity cache in response to any action with response.entities.
function entities(state = { users: {}, repos: {}, keywords: {} }, action) {
if (action.response && action.response.entities) {
return merge({}, state, action.response.entities)
}
return state
}
export default combineReducers({
locale,
router,
searchText,
errorMessage,
entities
})
My action:
import { CALL_API, Schemas } from '../middleware/api'
import isEmpty from 'lodash/isEmpty'
export const KEYWORDS_REQUEST = 'KEYWORDS_REQUEST'
export const KEYWORDS_SUCCESS = 'KEYWORDS_SUCCESS'
export const KEYWORDS_FAILURE = 'KEYWORDS_FAILURE'
// Fetches all keywords for pictos
// Relies on the custom API middleware defined in ../middleware/api.js.
function fetchKeywords() {
return {
[CALL_API]: {
types: [ KEYWORDS_REQUEST, KEYWORDS_SUCCESS, KEYWORDS_FAILURE ],
endpoint: 'users/56deee9a85cd6a05c58af61a',
schema: Schemas.KEYWORDS
}
}
}
// Fetches all keywords for pictograms from our API unless it is cached.
// Relies on Redux Thunk middleware.
export function loadKeywords() {
return (dispatch, getState) => {
const keywords = getState().entities.keywords
if (!isEmpty(keywords)) {
return null
}
return dispatch(fetchKeywords())
}
}
All based on the Real world redux example
My Solution
Given initial state to keywords entity. I'm getting json like this through ajax:
{'locale': 'en', 'keywords': ['keyword1', 'keyword2']}
However as I use normalizr with locale as id, for caching results, my initial state is as I describe in the reducer:
function entities(state = { users: {}, repos: {}, keywords: { 'en': { 'keywords': [] } } }, action) {
if (action.response && action.response.entities) {
return merge({}, state, action.response.entities)
}
return state
}
What I don't like is the initial if we have several languages, also remembering to modify it if we add another language, for example fr. In this
keywords: { 'en': { 'keywords': [] } }
should be:
keywords: { 'en': { 'keywords': [] }, 'fr': { 'keywords': [] } }
This line looks problematic:
const { keywords } = this.props.keywords
It's the equivalent of:
var keywords = this.props.keywords.keywords;
I doubt that's what you intended.
Another thing worth checking is keywords[locale] in your mapStateToProps() which will probably initially resolve to undefined. Make sure your component can handle that, or give it a sensible default.

Resources