I have various React components that can have different functionality when different props are passed in. Often I come across some branching logic where if prop1 is present, do one thing, but if prop2 is present do something else.
One example could be two change handlers for an element with an input that take different arguments.
Is there a way to specify in Flowjs that one of the two handlers are required?
Fortunately, the Flow type annotation that you want can be applied in contexts beyond React. Generally, if you want an exclusive-or type check you can do this:
type Something = {
prop1: number,
prop2?: null
}
type Another = {
prop1?: null,
prop2: number
}
function foo(parameter: Something | Another) {
}
foo({ prop1: 10 }); // Good
foo({ prop2: 10 }); // Good
foo({ prop1: 10, prop2: 10 }); // Bad
If you want an inclusive-or type check, you can do this:
type Something = {
prop1: number,
prop2?: null
}
type Another = {
prop1?: null,
prop2: number
}
type SomethingAndAnother = {
prop1: number,
prop2: number
}
function foo(parameter: Something | Another | SomethingAndAnother) {
}
foo({ something: 10, prop1: 10 }); // Good
foo({ something: 10 }); // Bad
foo({ prop1: 10 }); // Good
foo({ prop2: 10 }); // Good
foo({ prop1: 10, prop2: 10 }); // Good
In React, you define the type on the props property, like so:
class TodoList extends React.Component {
props: Something | Another;
render(): React.Element {
return (
// ....
)
}
}
How about an enum:
type Props = {
['prop1' | 'prop2']: string,
}
Related
I have an object in my pinia store like
import { defineStore } from "pinia";
export const useSearchStore = defineStore("store", {
state: () => {
return {
myobj: {
foo: 0,
bar: 2000,
too: 1000,
},
};
},
getters: {
changed() {
// doesn't work
return Object.entries(this.myobj).filter(([key, value]) => value != initialvalue
);
},
},
});
How do I get the initial value to test if the object changed. Or how can I return a filtered object with only those entries different from initial state?
My current workaround:
in a created hook I make a hard copy of the store object I then can compare to. I guess there is a more elegant way...
I had done this (although I do not know if there a better way to avoid cloning without duplicating your initial state).
Define your initial state outside and assign it to a variable as follows;
const initialState = {
foo: 0,
bar: 2000,
too: 1000
}
Then you can use cloning to retain the original state;
export const useSearchStore = defineStore("store", {
state: () => {
return {
myobj: structuredClone(initialState),
};
},
getters: {
changed: (state) => deepEquals(initialState, state.myobj);
},
});
where deepEquals is a method which deep compares the two objects (which you would have to implement). I would use lodash (npm i lodash and npm i #types/lodash --save-dev if you're using TypeScript) for this.
Full code (with lodash);
import { defineStore } from "pinia";
import { cloneDeep, isEqual } from "lodash";
const initialState = {
foo: 0,
bar: 2000,
too: 1000
}
export const useSearchStore = defineStore("store", {
state: () => ({
myobj: cloneDeep(initialState)
}),
getters: {
changed(state) {
return isEqual(initialState, state.myobj);
},
},
});
If you also want the differences between the two you can use the following function (the _ is lodash - import _ from "lodash");
function difference(object, base) {
function changes(object, base) {
return _.transform(object, function (result: object, value, key) {
if (!_.isEqual(value, base[key])) {
result[key] =
_.isObject(value) && _.isObject(base[key])
? changes(value, base[key])
: value;
}
});
}
return changes(object, base);
}
courtesy of https://gist.github.com/Yimiprod/7ee176597fef230d1451
EDIT:
The other way you would do this is to use a watcher to subscribe to changes. The disadvantage to this is that you either have to be OK with your state marked as "changed" if you change back the data to the initial state. Otherwise, you would have to implement a system (perhaps using a stack data structure) to maintain a list of changes so that if two changes which cancel each other out occur then you would remark the state as "unchanged". You would have to keep another variable (boolean) in the state which holds whether the state has been changed/unchanged - but this would be more complicated to implement and (depending on your use case) not worth it.
I have a Redux reducer for preferences and I am using Flow Type Checker. My reducer can take two types of actions. One for loading in all of the preferences which happens at initial app load. The second action type happens when the user updates a specific preference. Here is the code for my reducer. Where I run into problems is when I try to do action.prefs.forEach at which point flow throws an error saying ...'prefs': Property not found in 'object type'
// #flow
import {
UPDATE_PREF,
LOAD_PREFS_SUCCESS
} from '../actions/prefs';
export type actionType = {
+type: string,
prefs: Array<{_id: string, value: any}>
} | {
+type: string,
id: string,
value: any
};
export default (state: stateType = {}, action: actionType) => {
switch (action.type) {
case LOAD_PREFS_SUCCESS: {
const newState = {};
action.prefs.forEach(p => {
newState[p._id] = p.value;
});
return newState;
}
case UPDATE_PREF: {
return { ...state, [action.id]: action.value };
}
default:
return state;
}
};
As you can see I have two types of actions. When loading all of the preferences, the action has an array of preferences. [ { _id: 'color', value: 'blue' } ] And when updating a single preference, I get an id and a value. So give my two action types that have different properties, how do I get flow to not throw errors about this variation in action flow types?
In order to tell Flow which type to choose in the disjoint union, +type must be a value instead of string. Change your actionType to use values:
// #flow
import {
UPDATE_PREF,
LOAD_PREFS_SUCCESS
} from '../actions/prefs';
export type actionType = {
+type: LOAD_PREFS_SUCCESS, // not just `string`
prefs: Array<{_id: string, value: any}>
} | {
+type: UPDATE_PREF, // not just `string`
id: string,
value: any
};
Thanks to some guidance from #ross-allen and some playing around on flow.org I have found a working solution.
The short of it is that in addition to Ross' answer of adding +type: UPDATE_PREF, I also needed add typeof. So the working actionType is:
export type actionType = {
+type: typeof LOAD_PREFS_SUCCESS,
prefs: Array<{_id: string, value: any}>
} | {
+type: typeof UPDATE_PREF,
id: string,
value: any
};
Thanks again #ross-allen.
If I am interacting with an API that returns null for some objects that may or may not have value, how can I reconcile that with the reducers?
example: app state
{
foo: {
foo1: null,
foo2: {
bar: null,
bar2: null
}
}
}
but the server, when things are null, returns this:
{
foo: null
}
but it can return the full state when things have value:
{
foo: {
foo1: "somefoo,
foo2: {
bar: "barvalue,
bar2: 27
}
}
}
the problem I ham having is that my reducers are trying to load the state from the return value from the server, and then my components are trying to read from a null object and it is failing.
EDIT: the reducer and the component would look like this... so the component is trying to read some nested json, which may come back as unnreadable because the parent object is null. In this case I know I could hack up a solution that checks if the object is null and inserts my predefined initial state...
BUT...my actual example is a bigger json object than this and I know it will change in the future, so I need a solution that is not so fragile and cumbersome as adding a ton of logic here to check to make sure that every object down the nested like is not null.
var updateSettings = (settings = jsonShape, action) => {
swtich (action.type) {
case UPDATE_SETTINGS:
return Object.assign({}), settings, {
foo2: {
...settings.foo2,
bar: action.newBar
}
}
}
}
const Component = ({ settings }) => {
return (
<div>{ settings.foo2.bar }</div>
)
}
I'm new to flow, any trying to cover some of my functions, however often I have these snippets where I extract fields form an object based on some condition. But I'm struggling to cover them with flow.
const _join = function ( that: Array<Object>, by: string, index: number) {
that.forEach((thatOBJ: {[string]: any}, i: number)=>{
let obj: {[string]: any} = {};
for (let field: string in thatOBJ) {
if (field !== by) {
obj[`${index.toString()}_${field}`] = thatOBJ[field]; // NOT COVERED
} else {
obj[field] = thatOBJ[field]; // NOT COVERED
}
that[i] = obj;
}
});
}
The array that in this code is a data array so can really be in any format of mongodb data.
Any ideas on what to add to make the two lines which are not covered by flow covered?
Thanks.
A few notes...
This function has a "side effect" since you're mutating that rather than using a transformation and returning a new object.
Array<Object> is an Array of any, bounded by {}. There are no other guarantees.
If you care about modeling this functionality and statically typing them, you need to use unions (or |) to enumerate all the value possibilities.
It's not currently possible to model computed map keys in flow.
This is how I'd re-write your join function:
// #flow
function createIndexObject<T>(obj: { [string]: T }, by: string, index: number): { [string]: T } {
return Object.keys(obj).reduce((newObj, key) => {
if (key !== by) {
newObj[`${index}_${key}`] = newObj[key]
} else {
newObj[key] = obj[key]
}
return newObj
}, {})
}
// NO ERROR
const test1: { [string]: string | number } = createIndexObject({ foo: '', bar: 3 }, 'foo', 1)
// ERROR
const test2: { [string]: string | boolean } = createIndexObject({ foo: '', bar: 3 }, 'foo', 1)
Let's say I have this state:
state: {
field1: value1,
field2: {a: 5, b: 7}
}
If a reducer wants to update only field1, can the reducer return a new object containing a new field1 and the existing object state.field2 as field2 property of the new returned state? Or does the reducer have to clone field2?
Use spread operator
return {
...state,
field1: newVal
}
Here is the link detailed immutable update patterns
http://redux.js.org/docs/recipes/reducers/ImmutableUpdatePatterns.html
Yes. Not only is recycling state permissible, it's recommended.
Say your initial state is:
{
object1: { /* key-pair values */ },
object2: { /* key-pair values */ },
}
If you update your state like this:
// bad
return {
object1: action.object1,
object2: Object.assign({}, state.object2),
}
Then your app thinks object2 has changed even when it hasn't. This may cause unnecessary calculations and re-renders in your React components.
It's much better to only update the parts of your state that have actually changed.
// good
return Object.assign({}, state, {
object1: action.object1,
});
Object.assign() is what you are looking for, now you can also use the spread operator (...) , but be aware that those are ES6 features, so something like internet explorer (even 11) will crash at both , so you will need a polyfill, check this link it will give you more infos about it and also a polyfill for older browsers
https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Object/assign
var o1 = { a: 1 };
var o2 = { b: 2 };
var o3 = { b: 3 , c:2 };
var obj = Object.assign(o1, o2, o3);
console.log(obj); // { a: 1, b: 3 , c: 2 }
hope it helps