React hooks - indexed callback - reactjs

useCallback is useful for memoizing functions to be passed to sub-components, but what if you're rendering those sub-components in a loop? e.g.
const setBlockedDate = useCallback(idx => ev => {
setBlockedDates(bd => arraySplice(bd,idx,1,[ev.value]));
},[setBlockedDates])
...
{blockedDates.map((bd,idx) => <VehicleBlockedDate
key={bd[REACT_KEY]}
defaultValue={bd}
onChange={setBlockedDate(idx)}
onDelete={deleteBlockedDate(idx)}
reasons={[]}/>)}
useCallback will only memoize the outer function, but not the inner one. Is there a helper function for this pattern? Because I don't think this is right:
const setBlockedDate = useCallback(idx => useCallback(ev => {
setBlockedDates(bd => arraySplice(bd,idx,1,[ev.value]));
},[setBlockedDates]),[setBlockedDates])

I think this is correct:
function useArrayCallback(fn, deps = []) {
return useMemo(() => memoize(idx => (...args) => fn(idx, ...args)), deps);
}
Where memoize is:
function memoize(fn, options={
serialize: fn.length === 1 ? x => x : (...args) => JSON.stringify(args),
}) {
const cache = new Map();
return (...args) => {
const key = options.serialize(...args);
if(cache.has(key)) {
return cache.get(key);
}
const value = fn(...args);
cache.set(key, value);
return value;
}
}
Or you can use some fancy-pants prebuilt memoize function.
Then the callback in the original question can be written as:
const setBlockedDate = useArrayCallback((idx,ev) => {
setBlockedDates(bd => arraySplice(bd,idx,1,[ev.value]));
})

Related

How do i trigger a useEffect based on a sharedValue from the Reanimated libary

How do i trigger a useEffect based on a sharedValue from the Reanimated libary?
const sharedValue = useSharedValue(0);
useEffect(() => {
console.log("exectue functions");
}, [sharedValue.value]);
Is there a best practice for this. Or is there another way to trigger functions (sync and async) based on a change in a sharedValue.
You can use:
useAnimatedReaction;
useDerivedValue;
Solution with useAnimatedReaction
const sharedValue = useSharedValue(0);
useAnimatedReaction(() => {
return sharedValue.value;
}, (result, previous) => {
if (result !== previous) {
console.log(result)
}
}, []); // No need to pass dependencies
Solution with useDerivedValue
const sharedValue = useSharedValue(0);
useDerivedValue(() => {
console.log(sharedValue.value);
}, []); // No need to pass dependencies
useSharedValue in the Reanimated v2 library actually returns a reference and in react useEffect does not trigger on mutation in reference variable. So if you want to execute functions with changing useSharedValue I suggest you use useCallback or a function trigger.
EDIT:
UseCallback would only work for node references such as
const Component = () => {
const [isMounted, toggle] = useReducer((p) => !p, true);
const [elementRect, setElementRect] = useState();
const handleRect = useCallback((node) => {
setElementRect(node?.getBoundingClientRect());
}, []);
return (
<>
{isMounted && <div ref={handleRect}>Example</div>}
<button onClick={toggle}>Toggle</button>
<pre>{JSON.stringify(elementRect, null, 2)}</pre>
</>
);
};
If you want to make one with sharedRef then functional trigger is your only option:
const Component = () => {
const shared = useSharedValue(0);
const _incShared = () => shared.value++;
return(
<View></View>
)
}

Are React useEffect hooks guaranteed to execute in order?

Let's say I have this:
function Example(props) {
const { prop1, prop2 } = props;
const latestProp1 = useRef(prop1);
useEffect(() => {
latestProp1.current = prop1;
}, [prop1]);
useEffect(() => {
console.log(latestProp1.current);
}, [prop2]);
return null;
}
If both prop1 and prop2 change in the same re-render, is it guaranteed that the console would log the new prop1 value?
And if I swap the useEffect hooks' position -
useEffect(() => {
console.log(latestProp1.current);
}, [prop2]);
useEffect(() => {
latestProp1.current = prop1;
}, [prop1]);
would it be guaranteed that the console would log the old prop1 value (from before the render)?
Yes, if you follow the Rules of Hooks, the call order ensured (hooks are called via array index, check how hooks implemented).
By following this rule, you ensure that Hooks are called in the same order each time a component renders.
Therefore, by simulating a batched set state (both prop1 and prop2 change in the same re-render) you can notice that first will always be logged before second:
function Example(props) {
const { prop1, prop2 } = props;
React.useEffect(() => {
console.log("first");
}, [prop1]);
React.useEffect(() => {
console.log("second");
}, [prop2]);
return <>Example</>;
}
function Component() {
const [counter, setCounter] = React.useState(0);
const [counter2, setCounter2] = React.useState(0);
const together = () => {
setCounter2((p) => p + 1);
setCounter((p) => p + 1);
};
return (
<>
<Example prop1={counter} prop2={counter2} />
<button onClick={together}>together</button>
</>
);
}

Functions in a jest test only work when launched alone, but not at the same time

I have a custom hook that updates a state. The state is made with immer thanks to useImmer().
I have written the tests with Jest & "testing-library" - which allows to test hooks -.
All the functions work when launched alone. But when I launch them all in the same time, only the first one succeed. How so?
Here is the hook: (simplified for the sake of clarity):
export default function useSettingsModaleEditor(draftPage) {
const [settings, setSettings] = useImmer(draftPage);
const enablePeriodSelector = (enable: boolean) => {
return setSettings((draftSettings) => {
draftSettings.periodSelector = enable;
});
};
const enableDynamicFilter = (enable: boolean) => {
return setSettings((draftSettings) => {
draftSettings.filters.dynamic = enable;
});
};
const resetState = () => {
return setSettings((draftSettings) => {
draftSettings.filters.dynamic = draftPage.filters.dynamic;
draftSettings.periodSelector = draftPage.periodSelector;
draftSettings.filters.static = draftPage.filters.static;
});
};
return {
settings,
enablePeriodSelector,
enableDynamicFilter,
resetState,
};
}
And the test:
describe("enablePeriodSelector", () => {
const { result } = useHook(() => useSettingsModaleEditor(page));
it("switches period selector", () => {
act(() => result.current.enablePeriodSelector(true));
expect(result.current.settings.periodSelector).toBeTruthy();
act(() => result.current.enablePeriodSelector(false));
expect(result.current.settings.periodSelector).toBeFalsy();
});
});
describe("enableDynamicFilter", () => {
const { result } = useHook(() => useSettingsModaleEditor(page));
it("switches dynamic filter selector", () => {
act(() => result.current.enableDynamicFilter(true));
expect(result.current.settings.filters.dynamic).toBeTruthy();
act(() => result.current.enableDynamicFilter(false));
expect(result.current.settings.filters.dynamic).toBeFalsy();
});
});
describe("resetState", () => {
const { result } = useHook(() => useSettingsModaleEditor(page));
it("switches dynamic filter selector", () => {
act(() => result.current.enableDynamicFilter(true));
act(() => result.current.enablePeriodSelector(true));
act(() => result.current.addShortcut(Facet.Focuses));
act(() => result.current.resetState());
expect(result.current.settings.periodSelector).toBeFalsy();
expect(result.current.settings.filters.dynamic).toBeFalsy();
expect(result.current.settings.filters.static).toEqual([]);
});
});
All functions works in real life. How to fix this? Thanks!
use beforeEach and reset all mocks(functions has stale closure data) or make common logic to test differently and use that logic to test specific cases.
The answer was: useHook is called before "it". It must be called below.

Multiple state changes in event listener, how to NOT batch the DOM updates?

I'm building a component to test the performance of different algorithms. The algorithms return the ms they took to run and this is want I want to display. The "fastAlgorithm" takes about half a second, and the "slowAlgorithm" takes around 5 seconds.
My problem is that the UI is not re-rendered with the result until both algorithms have finished. I would like to display the result for the fast algorithm as soon as it finishes, and the slow algorithm when that one finishes.
I've read about how React batches updates before re-rendering, but is there someway to change this behavior? Or is there a better way to organize my component/s to achieve what I want?
I'm using react 16.13.1
Here is my component:
import { useState } from 'react'
import { fastAlgorithm, slowAlgorithm } from '../utils/algorithms'
const PerformanceTest = () => {
const [slowResult, setSlowResult] = useState(false)
const [fastResult, setFastResult] = useState(false)
const testPerformance = async () => {
fastAlgorithm().then(result => {
setFastResult(result)
})
slowAlgorithm().then(result => {
setSlowResult(result)
})
}
return (
<div>
<button onClick={testPerformance}>Run test!</button>
<div>{fastResult}</div>
<div>{slowResult}</div>
</div>
)
}
export default PerformanceTest
I read somewhere that ReactDOM.flushSync() would trigger the re-rendering on each state change, but it did not make any difference. This is what I tried:
const testPerformance = async () => {
ReactDOM.flushSync(() =>
fastAlgorithm().then(result => {
setFastResult(result)
})
)
ReactDOM.flushSync(() =>
slowAlgorithm().then(result => {
setSlowResult(result)
})
)
}
And also this:
const testPerformance = async () => {
fastAlgorithm().then(result => {
ReactDOM.flushSync(() =>
setFastResult(result)
)
})
slowAlgorithm().then(result => {
ReactDOM.flushSync(() =>
setSlowResult(result)
)
})
}
I also tried restructuring the algorithms so they didn't use Promises and tried this, with no luck:
const testPerformance = () => {
setFastResult(fastAlgorithm())
setSlowResult(slowAlgorithm())
}
Edit
As Sujoy Saha suggested in a comment below, I replaced my algorithms with simple ones using setTimeout(), and everything works as expected. "Fast" is displayed first and then two seconds later "Slow" is displayed.
However, if I do something like the code below it doesn't work. Both "Fast" and "Slow" shows up when the slower function finishes... Does anyone know exactly when/how the batch rendering in React happens, and how to avoid it?
export const slowAlgorithm = () => {
return new Promise((resolve, reject) => {
const array = []
for(let i = 0; i < 9000; i++) {
for(let y = 0; y < 9000; y++) {
array.push(y);
}
}
resolve('slow')
})
}
Your initial PerfomanceTest component is correct. The component will re-render for the each state change. I think issue is in your algorithm. Please let us know how did you returned promise there.
Follow below code snippet for your reference.
export const fastAlgorithm = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('fast')
}, 1000)
})
}
export const slowAlgorithm = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('slow')
}, 3000)
})
}
Are you running your algorithms synchronously on the main thread? If so, that's probably what's blocking React from re-rendering. You may need to move them to worker threads.
The below is loosely based on this answer, minus all the compatibility stuff (assuming you don't need IE support):
// `args` must contain all dependencies for the function.
const asyncify = (fn) => {
return (...args) => {
const workerStr =
`const fn = ${fn.toString()}
self.onmessage = ({ data: args }) => {
self.postMessage(fn(...args))
}`
const blob = new Blob([workerStr], { type: 'application/javascript' })
const worker = new Worker(URL.createObjectURL(blob))
let abort = () => {}
const promise = new Promise((resolve, reject) => {
worker.onmessage = (result) => {
resolve(result.data)
worker.terminate()
}
worker.onerror = (err) => {
reject(err)
worker.terminate()
}
// In case we need it for cleanup later.
// Provide either a default value to resolve to
// or an Error object to throw
abort = (value) => {
if (value instanceof Error) reject(value)
else resolve(value)
worker.terminate()
}
})
worker.postMessage(args)
return Object.assign(promise, { abort })
}
}
const multiplySlowly = (x, y) => {
const start = Date.now()
const arr = [...new Array(x)].fill([...new Array(y)])
return {
x,
y,
result: arr.flat().length,
timeElapsed: Date.now() - start,
}
}
const multiplySlowlyAsync = asyncify(multiplySlowly)
// rendering not blocked - just pretend this is React
const render = (x) => document.write(`<pre>${JSON.stringify(x, null, 4)}</pre>`)
multiplySlowlyAsync(999, 9999).then(render)
multiplySlowlyAsync(15, 25).then(render)
Note that fn is effectively being evaled in the context of the worker thread here, so you need to make sure the code is trusted. Presumably it is, given that you're already happy to run it on the main thread.
For completeness, here's a TypeScript version:
type AbortFn<T> = (value: T | Error) => void
export type AbortablePromise<T> = Promise<T> & {
abort: AbortFn<T>
}
// `args` must contain all dependencies for the function.
export const asyncify = <T extends (...args: any[]) => any>(fn: T) => {
return (...args: Parameters<T>) => {
const workerStr =
`const fn = ${fn.toString()}
self.onmessage = ({ data: args }) => {
self.postMessage(fn(...args))
}`
const blob = new Blob([workerStr], { type: 'application/javascript' })
const worker = new Worker(URL.createObjectURL(blob))
let abort = (() => {}) as AbortFn<ReturnType<T>>
const promise = new Promise<ReturnType<T>>((resolve, reject) => {
worker.onmessage = (result) => {
resolve(result.data)
worker.terminate()
}
worker.onerror = (err) => {
reject(err)
worker.terminate()
}
// In case we need it for cleanup later.
// Provide either a default value to resolve to
// or an Error object to throw
abort = (value: ReturnType<T> | Error) => {
if (value instanceof Error) reject(value)
else resolve(value)
worker.terminate()
}
})
worker.postMessage(args)
return Object.assign(promise, { abort }) as AbortablePromise<
ReturnType<T>
>
}
}

Modifying object inside array with useContext

I've been having trouble using React's useContext hook. I'm trying to update a state I got from my context, but I can't figure out how. I manage to change the object's property value I wanted to but I end up adding another object everytime I run this function. This is some of my code:
A method inside my "CartItem" component.
const addToQuantity = () => {
cartValue.forEach((item) => {
let boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
setCartValue((currentState) => [...currentState, item.quantity++])
} else {
return null;
}
});
};
The "Cart Component" which renders the "CartItem"
const { cart, catalogue } = useContext(ShoppingContext);
const [catalogueValue] = catalogue;
const [cartValue, setCartValue] = cart;
const quantiFyCartItems = () => {
let arr = catalogueValue.map((item) => item.name);
let resultArr = [];
arr.forEach((item) => {
resultArr.push(
cartValue.filter((element) => item === element.name).length
);
});
return resultArr;
};
return (
<div>
{cartValue.map((item, idx) => (
<div key={idx}>
<CartItem
name={item.name}
price={item.price}
quantity={item.quantity}
id={item.id}
/>
<button onClick={quantiFyCartItems}>test</button>
</div>
))}
</div>
);
};
So how do I preserve the previous objects from my cartValue array and still modify a single property value inside an object in such an array?
edit: Here's the ShoppingContext component!
import React, { useState, createContext, useEffect } from "react";
import axios from "axios";
export const ShoppingContext = createContext();
const PRODUCTS_ENDPOINT =
"https://shielded-wildwood-82973.herokuapp.com/products.json";
const VOUCHER_ENDPOINT =
"https://shielded-wildwood-82973.herokuapp.com/vouchers.json";
export const ShoppingProvider = (props) => {
const [catalogue, setCatalogue] = useState([]);
const [cart, setCart] = useState([]);
const [vouchers, setVouchers] = useState([]);
useEffect(() => {
getCatalogueFromApi();
getVoucherFromApi();
}, []);
const getCatalogueFromApi = () => {
axios
.get(PRODUCTS_ENDPOINT)
.then((response) => setCatalogue(response.data.products))
.catch((error) => console.log(error));
};
const getVoucherFromApi = () => {
axios
.get(VOUCHER_ENDPOINT)
.then((response) => setVouchers(response.data.vouchers))
.catch((error) => console.log(error));
};
return (
<ShoppingContext.Provider
value={{
catalogue: [catalogue, setCatalogue],
cart: [cart, setCart],
vouchers: [vouchers, setVouchers],
}}
>
{props.children}
</ShoppingContext.Provider>
);
};
edit2: Thanks to Diesel's suggestion on using map, I came up with this code which is doing the trick!
const newCartValue = cartValue.map((item) => {
const boolean = Object.values(item).includes(props.name);
if (boolean && item.quantity < item.available) {
item.quantity++;
}
return item;
});
removeFromStock();
setCartValue(() => [...newCartValue]);
};```
I'm assuming that you have access to both the value and the ability to set state here:
const addToQuantity = () => {
cartValue.forEach((item) => {
let boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
setCartValue((currentState) => [...currentState, item.quantity++])
} else {
return null;
}
});
};
Now... if you do [...currentState, item.quantity++] you will always add a new item. You're not changing anything. You're also running setCartValue on each item, which isn't necessary. I'm not sure how many can change, but it looks like you want to change values. This is what map is great for.
const addToQuantity = () => {
setCartValue((previousCartValue) => {
const newCartValue = previousCartValue.map((item) => {
const boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
return item.quantity++;
} else {
return null;
}
});
return newCartValue;
});
};
You take all your values, do the modification you want, then you can set that as the new state. Plus it makes a new array, which is nice, as it doesn't mutate your data.
Also, if you know only one item will ever match your criteria, consider the .findIndex method as it short circuits when it finds something (it will stop there), then modify that index.

Resources