React. Rerendering nested array - javascript

I am building quiz app and there is a problem that i have to shuffle question options but they are located in the nested question array. Here is the interface:
export interface IConnectionsQuestionData extends IQuestionData {
type: 'connections';
questionsTitle: string;
answersTitle: string;
pairs: {
questionOption: IConnectionsOption;
answerOption: Maybe<IConnectionsOption>;
}[];
}
export interface IConnectionsOption {
key: IDType;
value: string;
type: 'answer' | 'question'
}
So in order to shuffle options i have created custom useShuffle hook:
export const useShuffle = <T>(array: T[]): T[] => {
return useMemo(() => {
return shuffleArray(array);
}, [array])
}
In Question component I get question from props. I use this hook like this:
const shuffledLeftSideOptions = useShuffle(question?.pairs.map(pair => pair.questionOption) ?? [])
const shuffledRightSideOptions = useShuffle(question?.pairs.map(pair => pair.answerOption) ?? [])
But every time component need to be rerendered when i choose the option, options array shuffles again on every render. I tried to test it and it works fine with pairs array but reshuffles with question or answer options.

You've only memoized the shuffling, not the map operation. So you get a new array every time, because map produces a new array every time, and so useShuffle has to do its work every time.
If you need to do the map, you'll need to memoize that as well, e.g.:
const mappedLeftOptions = useMemo(
() => question?.pairs.map((pair) => pair.questionOption) ?? [],
[question]
);
const mappedRightOptions = useMemo(
() => question?.pairs.map((pair) => pair.answerOption) ?? [],
[question]
);
const shuffledLeftSideOptions = useShuffle(mappedLeftOptions);
const shuffledRightSideOptions = useShuffle(mappedRightOptions);
But, beware that the memoization provided by useMemo provides only a performance optimization, not a semantic guarantee. (More in the documentation.) But your shuffling code is relying on it as though it were a semantic guarantee.
Instead, when you need a semantic guarantee, use a ref:
export const useShuffle = <T>(array: T[]): T[] => {
const arrayRef = useRef<T[] | null>(null);
const shuffleRef = useRef<T[] | null>(null);
if (!Object.is(array, arrayRef.current)) {
shuffleRef.current = null;
}
if (!shuffleRef.current) {
arrayRef.current = array;
shuffleRef.current = shuffleArray(array);
}
return shuffleRef.current;
};
Or it's probably cleaner with a single ref:
export const useShuffle = <T>(array: T[]): T[] => {
const ref = useRef<{ array: T[]; shuffled: T[] } | null>(null);
if (!ref.current || !Object.is(ref.current.array, array)) {
ref.current = {
array,
shuffled: shuffleArray(array),
};
}
return ref.current.shuffled;
};

Related

Typescript: Map object props types in wrapped function to omit certain props

I'm trying to wrap an actions object with functions as properties. Each function has an ActionContext argument.
When defining the functions the state property in the context should be accessible. But when calling the method state should not be accessible anymore, as it will be injected.
I first tried to do this with 2 separate args in each function, but that felt wrong and seemed not flexible enough. Therefor I'm using just 1 ctx argument now.
Solution approach #1: How can I dynamically re-map an object full of functions using TypeScript?
The return type on useActions = (state: any): T is ofcourse not correct, but here I want the types in the actions-object to be mapped from type Action to type CallableAction with their inferred Context-type.
But I'm struggling how to infer these types, if at all possible. Or maybe I'm just Typing too much? Quite new to typescript.
type ActionContext<S=any, P=any> = { state: S, params: P };
type Action = <T extends ActionContext>(ctx: T) => any;
type CallableActionContext<T> = T extends ActionContext ? Omit<T, 'state'> : never
type CallableAction<T=any> = (ctx: CallableActionContext<T>) => any
type SaveActionContext = ActionContext<any, {id: string}>
interface Supplier {
name: string;
}
// add ctx types in actions
const actions = {
save: async (ctx: SaveActionContext): Promise<Supplier> => {
console.log('Mocked: useActions.save');
const tralala = ctx.params.id
await Promise.resolve(tralala)
return {name: 's1'};
}
}
// this is what the resulting action object should look like
// calling mapped actions and omitting `state` property):
// const save: CallableAction<SaveActionContext> = (ctx) => {
// ctx.params.id
// return new SupplierVM
// }
// const ret = save({params:{id: '2'}})
const createActions = <T extends Record<keyof T, Action>>(actions: T) => {
const useActions = (state: any): T => {
const injectCtx = (action: Action, ctx: any) => {
const enrichedCtx = {...ctx, state: state} as ActionContext
return action(enrichedCtx)
}
return Object.entries(actions).reduce((prev: any, [fnName, fn]: any) => ({
...prev,
[fnName]: (ctx: any) => injectCtx(fn, ctx)
}), {});
}
return useActions
}
const state = reactive({})
const useActions = createActions(actions)
const acts = useActions(state)
acts.save({params: { id: '3'}, state: {}})
Perhaps you should change (state: any): T:
const useActions = (state: any): { [K in keyof T]: CallableAction<Parameters<T[K]>[0]> } => {
We're going through each action, getting the first parameter, (SaveActionContext), then we pass that to CallableAction which gives us our desired context type.
Playground

replacing enum - better solution needed

I have an app that uses React Native' Native Module.
Currently the logic is that the Data type will be obtained from my enum as shown below
export declare enum HKQuantityTypeIdentifier {
HeartRate = "HKQuantityTypeIdentifierHeartRate"
}
const requestAuthorization = (
read: (HKQuantityTypeIdentifier)[],
write: HKQuantityTypeIdentifier[] = []
): Promise<boolean> => {
const readAuth = read.reduce((obj, cur) => {
return { ...obj, [cur]: true };
}, {});
const writeAuth = write.reduce((obj, cur) => {
return { ...obj, [cur]: true };
}, {});
return NativeModule.requestAuthorization(writeAuth, readAuth);
};
const MyHealthLibrary: ReactNativeHealthkit = {
requestAuthorization
}
export default MyHealthLibrary;
Front-end call:
await MyHealthLibrary.requestAuthorization([HKQuantityTypeIdentifier.HeartRate]);
This will give my expected result
Now i do not want to call the function from my front-end with the entire type "HKQuantityTypeIdentifier.HeartRate" instead i just want to call it like "HeartRate". Something like below:
await MyHealthLibrary.requestAuthorization(["HeartRate"]);
How do i achieve this??? Any help would be great!
I don't understand your issue with enums, but you can easily do that this way then:
export type HKQuantityTypeIdentifier = 'HKQuantityTypeIdentifierHeartRate' | 'HKQuantityTypeIdentifierOtherData';
const requestAuthorization = (
read: (HKQuantityTypeIdentifier)[],
write: HKQuantityTypeIdentifier[] = []
): Promise<boolean> => {
Assuming that you want to be able to extend your set of values, you could replace the enum by a ts-file that exports constants. It is not state of the art but an alternative.
TS-File
my-constants.ts
export const HEART_RATE = "HeartRate";
export const CO2 = "CO2";
// and so forth
Your component
import { HEART_RATE, CO2 } from 'my-constants.ts';
// ...
await MyHealthLibrary.requestAuthorization([HEART_RATE]);

React useState array update problem via context

I have an array that via useState hook, I try to add elements to the end of this array via a function that I make available through a context. However the array never gets beyond length 2, with elements [0, n] where n is the last element that I pushed after the first.
I have simplified this a bit, and haven't tested this simple of code, it isn't much more complicated though.
MyContext.tsx
interface IElement = {
title: string;
component: FunctionComponent<any>;
props: any;
}
interface IMyContext = {
history: IElement[];
add: <T>(title: string, component: FunctionComponent<T>, props: T) => void
}
export const MyContext = React.createContext<IMyContext>({} as IMyContext)
export const MyContextProvider = (props) => {
const [elements, setElements] = useState<IElement[]>([]);
const add = <T extends any>(title: string, component: FunctionComponent<T>, props: T) => {
setElements(elements.concat({title, component, props}));
}
return (
<MyContext.Provider values={{elements, add}}>
{props.children}
</MyContext.Provider>
);
}
In other elements I use this context to add elements and show the current list of elements, but I only ever get 2, no matter how many I add.
I add via onClick from various elements and I display via a sidebar that uses the components added.
SomeElement.tsx
const SomeElement = () => {
const { add } = useContext(MyContext);
return (
<Button onClick=(() => {add('test', MyDisplay, {id: 42})})>Add</Button>
);
};
DisplayAll.tsx
const DisplayAll = () => {
const { elements } = useContext(MyContext);
return (
<>
{elements.map((element) => React.createElement(element.component, element.props))}
</>
);
};
It sounds like your issue was in calling add multiple times in one render. You can avoid adding a ref as in your currently accepted answer by using the callback version of setState:
const add = <T extends any>(title: string, component: FunctionComponent<T>, props: T) => {
setElements(elements => elements.concat({title, component, props}));
};
With the callback version, you'll always be sure to have a reference to the current state instead of a value in your closure that may be stale.
The below may help if you're calling add multiple times within the same render:
interface IElement = {
title: string;
component: FunctionComponent<any>;
props: any;
}
interface IMyContext = {
history: IElement[];
add: <T>(title: string, component: FunctionComponent<T>, props: T) => void
}
export const MyContext = React.createContext<IMyContext>({} as IMyContext)
export const MyContextProvider = (props) => {
const [elements, setElements] = useState<IElement[]>([]);
const currentElements = useRef(elements);
const add = <T extends any>(title: string, component: FunctionComponent<T>, props: T) => {
currentElements.current = [...currentElements.current, {title, component, props}];
setElements(currentElements.current);
}
return (
<MyContext.Provider values={{history, add}}>
{props.children}
</MyContext.Provider>
);
}

How to correctly use a curried selector function with react-redux's useSelector hook?

I am using react-redux with hooks, and I need a selector that takes a parameter that is not a prop. The documentation states
The selector function does not receive an ownProps argument. However,
props can be used through closure (see the examples below) or by using
a curried selector.
However, they don't provide an example. What is the proper way to curry as described in the docs?
This is what I've done and it seems to work, but is this right? Are there implications from returning a function from the useSelector function (it seems like it would never re-render?)
// selectors
export const getTodoById = state => id => {
let t = state.todo.byId[id];
// add display name to todo object
return { ...t, display: getFancyDisplayName(t) };
};
const getFancyDisplayName = t => `${t.id}: ${t.title}`;
// example component
const TodoComponent = () => {
// get id from react-router in URL
const id = match.params.id && decodeURIComponent(match.params.id);
const todo = useSelector(getTodoById)(id);
return <span>todo.display</span>;
}
When the return value of a selector is a new function, the component will always re-render on each store change.
useSelector() uses strict === reference equality checks by default, not shallow equality
You can verify this with a super simple selector:
const curriedSelector = state => () => 0;
let renders = 0;
const Component = () => {
// Returns a new function each time
// triggers a new render each time
const value = useSelector(curriedSelector)();
return `Value ${value} (render: ${++renders})`;
}
Even if the value is always 0, the component will re-render on each store action since useSelector is unaware that we're calling the function to get the real value.
But if we make sure that useSelector receives the final value instead of the function, then the component only gets rendered on real value change.
const curriedSelector = state => () => 0;
let renders = 0;
const Component = () => {
// Returns a computed value
// triggers a new render only if the value changed
const value = useSelector(state => curriedSelector(state)());
return `Value ${value} (render: ${++renders})`;
}
Conclusion is that it works, but it's super inefficient to return a new function (or any new non-primitives) from a selector used with useSelector each time it is called.
props can be used through closure (see the examples below) or by using a curried selector.
The documentation meant either:
closure useSelector(state => state.todos[props.id])
curried useSelector(state => curriedSelector(state)(props.id))
connect is always available, and if you changed your selector a little, it could work with both.
export const getTodoById = (state, { id }) => /* */
const Component = props => {
const todo = useSelector(state => getTodoById(state, props));
}
// or
connect(getTodoById)(Component)
Note that since you're returning an Object from your selector, you might want to change the default equality check of useSelector to a shallow equality check.
import { shallowEqual } from 'react-redux'
export function useShallowEqualSelector(selector) {
return useSelector(selector, shallowEqual)
}
or just
const todo = useSelector(state => getTodoById(state, id), shallowEqual);
If you're performing costly computations in the selector or the data is deeply nested and performance becomes a problem, take a look at Olivier's answer which uses memoization.
Here is a solution, it uses memoïzation to not re-render the component on each store change :
First I create a function to make selectors, because the selector depends on the component property id, so I want to have a new selector per component instances.
The selector will prevent the component to re-render when the todo or the id prop hasn't changed.
Lastly I use useMemo because I don't want to have more than one selector per component instance.
You can see the last example of the documentation to have more information
// selectors
const makeGetTodoByIdSelector = () => createSelector(
state => state.todo.byId,
(_, id) => id,
(todoById, id) => ({
...todoById[id],
display: getFancyDisplayName(todoById[id])
})
);
const getFancyDisplayName = t => `${t.id}: ${t.title}`;
// example component
const TodoComponent = () => {
// get id from react-router in URL
const id = match.params.id && decodeURIComponent(match.params.id);
const getTodoByIdSelector = useMemo(makeGetTodoByIdSelector, []);
const todo = useSelector(state => getTodoByIdSelector(state, id));
return <span>todo.display</span>;
}
Yes, it is how it's done, simplified example:
// Curried functions
const getStateById = state => id => state.todo.byId[id];
const getIdByState = id => state => state.todo.byId[id];
const SOME_ID = 42;
const TodoComponent = () => {
// id from API
const id = SOME_ID;
// Curried
const todoCurried = useSelector(getStateById)(id);
const todoCurried2 = useSelector(getIdByState(id));
// Closure
const todoClosure = useSelector(state => state.todo.byId[id]);
// Curried + Closure
const todoNormal = useSelector(state => getStateById(state)(id));
return (
<>
<span>{todoCurried.display}</span>
<span>{todoCurried2.display}</span>
<span>{todoClosure.display}</span>
<span>{todoNormal.display}</span>
</>
);
};
Full example:
This is helper-hook useParamSelector for TypeScript, which implements the official approach of Redux Toolkit.
Hook implementation:
// Define types and create new hook
export type ParametrizedSelector<A, R> = (state: AppState, arg: A) => R;
export const proxyParam: <T>(_: AppState, param: T) => T = (_, param) => param;
export function useParamSelector<A, R>(
selectorCreator: () => ParametrizedSelector<A, R>,
argument: A,
equalityFn: (left: R, right: R) => boolean = shallowEqual
): R {
const memoizedSelector = useMemo(() => {
const parametrizedSelector = selectorCreator();
return (state: AppState) => parametrizedSelector(state, argument);
}, [typeof argument === 'object' ? JSON.stringify(argument) : argument]);
return useSelector(memoizedSelector, equalityFn);
}
Create parametrized selector:
export const selectUserById = (): ParametrizedSelector<string, User> =>
createSelector(proxyParam, selectAllUsers, (id, users) => users.find((it) => it.id === id));
And use it:
const user = useParamSelector(selectUserById, 1001); // in components
const user = selectUserById()(getState(), 1001); // in thunks
You can also use it hook with selectors created with reselect's createSelector.

Typescript Generics wrapper: Untyped function calls may not accept type arguments

This code snippet with Generics works perfectly fine (Link to Simple and Working Code)
const state: Record<string, any> = {
isPending: false,
results: ['a', 'b', 'c']
}
const useValue = <T extends {}>(name: string): [T, Function] => {
const value: T = state[name]
const setValue: Function = (value: T): void => {
state[name] = value
}
return [value, setValue]
}
const [isPending, setIsPending] = useValue<boolean>('isPending')
const [results, setResults] = useValue<string[]>('results')
Here i can be sure that isPending is a boolean and setIsPending receives a boolean as parameter. Same applies to results and setResults as an array of strings.
Then I wrap the code with another method useStore (Link to Extended Broken Code)
interface UseStore {
state: Record<string, any>
useValue: Function
}
const state: Record<string, any> = {
isPending: false,
results: ['a', 'b', 'c']
}
const useStore = (): UseStore => {
const useValue = <T extends {}>(name: string): [T, Function] => {
const value: T = state[name]
const setValue: Function = (value: T): void => {
state[name] = value
}
return [value, setValue]
}
return { state, useValue }
}
const { useValue } = useStore()
const [isPending, setIsPending] = useValue<boolean>('isPending')
const [results, setResults] = useValue<string[]>('results')
The last two lines give me typescript errors: Untyped function calls may not accept type arguments.
I suspect the useStore interface to be problematic, but due to the dynamic nature of it i can't think of a better solution.
How can I get rid of the errors while using the Generic type to get proper type hints and code completion?
Since the type of useValue is Function it makes no sense to pass in generic type arguments. Who do they benefit? The runtime doesn't get them, they are erased at compiler time. The compiler can't use them since Function is just an un-typed function, so there is no benefit there. Passing type arguments is useless and arguably a mistake (ie the user was not expecting this to be Function and is passing in the type arguments thinking they have some effect).
Remove the type arguments and drop the pretense that this is in any way typed:
const { useValue } = useStore()
const [isPending, setIsPending] = useValue('isPending')
const [results, setResults] = useValue('results')
The more interesting question is why are you writing the code like this since there is a way to fully type everything in this code:
const state = {
isPending: false,
results: ['a', 'b', 'c']
}
type State = typeof state;
const useStore = () => {
const useValue = <K extends keyof State>(name: K) => {
const value = state[name]
const setValue = (value: State[K]): void => {
state[name] = value
}
return [value, setValue] as const
}
return { state, useValue }
}
type UseStore = ReturnType<typeof useStore>;
const { useValue } = useStore()
const [isPending, setIsPending] = useValue('isPending')
const [results, setResults] = useValue('results')
The version above is fully type safe and does not require any duplication of names or types (You could of course split this out in multiple files, but then probably some duplication would need to occur depending on your requirements). If this is not applicable in your case, I would be interested to know why.
Edit
If you just want the types to work out on the last lines and have some type safety there, you just need to specify the signature for the functions using generics:
interface UseStore {
state: Record<string, any>
useValue: <T,>(name: string) => [T, (value: T)=> void]
}
const state: Record<string, any> = {
isPending: false,
results: ['a', 'b', 'c']
}
const useStore = (): UseStore => {
const useValue = <T,>(name: string): [T, (value: T)=> void] => {
const value: T = state[name]
const setValue = (value: T): void => {
state[name] = value
}
return [value, setValue]
}
return { state, useValue }
}
const { useValue } = useStore()
const [isPending, setIsPending] = useValue<boolean>('isPending')
const [results, setResults] = useValue<string[]>('results')
Edit - An open ended in interface implementation
You can define State as an interface, since interfaces are open-ended you can add members when needed. The benefit is if someone else defines a property with the same name but a different type you get an error
interface State {
}
// Don't know what is in here, empty object for starters
const state : State = {
} as State
const useStore = () => {
const useValue = <K extends keyof State>(name: K) => {
const value = state[name]
const setValue = (value: State[K]): void => {
state[name] = value
}
return [value, setValue] as const
}
return { state, useValue }
}
type UseStore = ReturnType<typeof useStore>;
const { useValue } = useStore()
interface State { isPending: boolean }
state.isPending = false; // no guarantee people actually intialize, maybe initial value can be passed to useValue ?
const [isPending, setIsPending] = useValue('isPending')
interface State { results: string[] }
state.results = ['a', 'b', 'c'];
const [results, setResults] = useValue('results')
interface State { results: number[] } // some else wants to use results for something else, err
const [results2, setResults2] = useValue('results')

Categories

Resources