Toolbelt for more flexible effects in react
npm install --save react-delta
By default, react functional components hide as much information as possible about adjacent renders. Refs are available if you want access to values from previous renders or even values from future renders, but useEffect
dependency arrays are the primary mechanism provided to compare values to those in the previous render. If anything in the dependency array has changed, the effect will run. react-delta
provides hooks to access the previous, current, and future values of a variable and the ability to compare these values in whichever fashion you'd like. Once you've figured out how the new values compare to the old, you can use a simple boolean to decide whether an effect should run.
If you've used useEffect
in your day-to-day, you've surely found yourself in tricky situations. For example, maybe you've wanted access to a value from a previous render to know how a variable has changed since the last render and not just that it has changed. Or maybe the linter has yelled at you to include all dependencies in the useEffect
dependency array, but doing so would cause your effect to run too frequently. Or maybe you've wanted to use deep equality to trigger an effect instead of shallow equality. Or maybe you've had to store values in refs in order to access the latest value inside of useEffect
after a long asynchronous action. react-delta
aims to alleviate some of these pains in a clean and concise way.
You want to log when the window width has increased and when it has decreased. Below we see how we might approach this problem traditionally, and how we can better approach it using react-delta
.
function useWindowLogger() {
const { width } = useWindowSize();
const prevWidth = useRef();
useEffect(() => {
if (prevWidth.current && width != prevWidth.current) {
if (width < prevWidth.current) {
console.log("Window got narrower");
} else {
console.log("Window got wider");
}
}
}, [prevWidth.current, width]);
useEffect(() => {
prevWidth.current = width;
}, [width]);
}
import { useDelta } from 'react-delta';
function useWindowLogger() {
const { width } = useWindowSize();
const delta = useDelta(width)
useEffect(() => {
if (delta && delta.prev) {
if (delta.prev < delta.curr) {
console.log("Window got narrower");
} else {
console.log("Window got wider");
}
}
});
}
import { usePrevious } from 'react-delta';
function useWindowLogger() {
const { width } = useWindowSize();
const prevWidth = usePrevious(width);
useEffect(() => {
if (prevWidth && prevWidth !== width) {
if (width < prevWidth) {
console.log("Window got narrower");
} else {
console.log("Window got wider");
}
}
});
}
import { useDelta, useConditionalEffect } from 'react-delta';
function useWindowLogger() {
const { width } = useWindowSize();
const delta = useDelta(width);
useConditionalEffect(() => {
console.log("Window got narrower");
}, delta && delta.prev && delta.curr < delta.prev);
useConditionalEffect(() => {
console.log("Window got wider");
}, delta && delta.prev && delta.curr > delta.prev);
}
You want to log only when both width and height of the window have changed, but not if only one has changed.
import { useDeltaArray, every, useConditionalEffect } from 'react-delta';
function useWindowLogger() {
const { width, height } = useWindowSize();
const deltas = useDeltaArray([width, height]);
useConditionalEffect(() => {
console.log("Window width and height changed simultaneously");
}, every(deltas));
}
You want to set up an interval when the component mounts and its callback needs access to data from future renders.
import { useLatest } from 'react-delta';
function useIntervalLogger(data) {
const dataRef = useLatest(data);
useEffect(() => {
const id = setInterval(() => {
console.log(dataRef.current);
}, 3000);
return () => clearInterval(id);
}, []);
}
See this playground to mess around with react-delta
.
- usePrevious
- useLatest
- useDelta
- useDeltaObject
- useDeltaArray
- useConditionalEffect
- useConditionalLayoutEffect
- some
- every
Gets the value from the previous render of the observed variable.
usePrevious<T>(value: T): Optional<T>;
value
: required - a variable to observe across renders.
The value passed to this hook during the previous render or undefined (if the first render).
import { usePrevious } from 'react-delta';
function useWindowLogger() {
const { width } = useWindowSize();
const prevWidth = usePrevious(width);
useEffect(() => {
if (prevWidth && prevWidth !== width) {
if (width < prevWidth) {
console.log("Window got narrower");
} else {
console.log("Window got wider");
}
}
});
}
Gets a ref which always points to the value from the most recent render. This is useful when you want to access a value from a future render inside of an older render.
useLatest<T>(value: T): MutableRefObject<T>;
value
: required - a variable to observe across renders.
A ref to the value passed to this hook in the most recent render.
import { useLatest } from 'react-delta';
function useIntervalLogger(data) {
const dataRef = useLatest(data);
useEffect(() => {
// The interval will only be set up on mount,
// but ref will allow interval callback to
// access latest data value through the lifetime
// of the component.
const id = setInterval(() => {
console.log(dataRef.current);
}, 3000);
return () => clearInterval(id);
}, []);
}
Determines the delta of value
between the current and the previous render.
useDelta<T>(value: T, options?: { deep?: boolean }): Nullable<Delta<T>>;
value
: required - a variable to observe across renders.options
: optional [default { deep: false }]deep
: a boolean indicating whether to use deep equality when comparing the current value to the previous value.
If the observed variable has changed between the current and the previous render, a delta object is returned. If nothing has changed, then null
is returned.
import { useDelta, useConditionalEffect } from 'react-delta'
const useFetch = (url) => {
const delta = useDelta(url)
useConditionalEffect(() => {
fetch(url)
}, !!delta)
}
Determines the deltas of the values of the passed object. This is useful for observing many variables at once. For example, you could use this hook to find the deltas of all props.
Note: Only the keys of the object passed during the first render will be observed. If different keys are passed after the first render, they will be ignored. For this reason, it is recommend that you explicitly pass object keys (eg. useDeltaObject({ foo: props.foo, bar: props.bar })
as opposed to useDeltaObject(props)
).
useDeltaObject<T extends {}>(obj: T, options?: { deep?: boolean }): DeltaObject<T>;
obj
: required - an object whose values will be observed across renders.options
: optional [default { deep: false }]deep
: a boolean indicating whether to use deep equality when comparing the current values to the previous values.
An object with the same keys as the passed object, but whose values represent the deltas of the passed object's values.
import { useDeltaObject, some, useConditionalEffect } from 'react-delta'
const LogPropsOnChange = (props) => {
const deltas = useDeltaObject(props)
useConditionalEffect(() => {
console.log('At least one prop changed')
}, some(Object.values(deltas)))
return null
}
Determines the deltas of the values of the passed array. This is useful for observing many values at once.
Note: Only the indexes of the array passed during the first render will be observed. If an array of greater length is passed after the first render, the extra indexes will be ignored. For this reason, it is recommend that you explicitly pass array indexes (eg. useDeltaArray([props.foo, props.bar])
as opposed to useDeltaArray(Object.values(props))
).
useDeltaArray<T extends any[]>(array: T, options?: { deep?: boolean }): DeltaArray<T>;
array
: required - an array whose values will be observed across renders.options
: optional [default { deep: false }]deep
: a boolean indicating whether to use deep equality when comparing the current values to the previous values.
An array with the same length as the passed array, but whose values represent the deltas of the passed arrays's values.
import { useDeltaArray, some, useConditionalEffect } from 'react-delta'
const FooFetcher = ({page, search}) => {
const [pageDelta, searchDelta] = useDeltaArray([page, search])
useConditionalEffect(() => {
fetch(`http://foo.com?search=${search}&page=${page}`)
}, some([pageDelta, searchDelta]))
return null
}
Runs an effect when the condition is truthy. If the effect returns a cleanup function, the cleanup function will run before the next effect.
useConditionalEffect(callback: ConditionalEffectCallback, condition: any): void;
callback
: required - a function that will execute if the condition is truthy. This callback can return a cleanup function.condition
: required - a value indicating whether the effect should run. The effect callback executes when the condition is truthy.
This method has no return value.
import { useConditionalEffect } from 'react-delta'
const useEvenCountLogger = (count) => {
useConditionalEffect(() => {
console.log(count)
}, count % 2 === 0)
}
Runs a layout effect when the condition is truthy. If the effect returns a cleanup function, the cleanup function will run before the next layout effect.
useConditionalLayoutEffect(callback: ConditionalLayoutEffectCallback, condition: any): void;
callback
: required - a function that will execute if the condition is truthy. This callback can return a cleanup function.condition
: required - a boolean indicating whether the layout effect should run. The layout effect callback executes when the condition is truthy.
This method has no return value.
import { useConditionalLayoutEffect } from 'react-delta'
const useEvenCountLogger = (count) => {
useConditionalLayoutEffect(() => {
console.log(count)
}, count % 2 === 0)
}
Indicates whether some value in the array is truthy.
some(array: any[]): boolean;
array
: required - an array of values that will be coerced to booleans.
Returns true if any value in the array is truthy. Returns false if all values in the array are false or if the array is empty.
import { some, useDeltaObject, useConditionalEffect } from "react-delta";
const SomePropChangeLogger = props => {
const deltas = useDeltaObject(props);
useConditionalEffect(() => {
console.log("At least one prop changed");
}, some(Object.values(deltas)));
return null;
};
Indicates whether every value in the array is truthy.
every(array: any[]): boolean;
array
: required - an array of values that will be coerced to booleans.
Returns true if every value in the array is truthy or if the array is empty. Returns false if any value in the array is false.
import { every, useDeltaObject, useConditionalEffect } from "react-delta";
const EveryPropChangeLogger = props => {
const deltas = useDeltaObject(props);
useConditionalEffect(() => {
console.log("Every prop changed simultaneously");
}, every(Object.values(deltas)));
return null;
};
interface Delta<T> {
prev?: T;
curr: T;
}
type DeltaArray<T extends any[]> = {
[k in keyof T]: Nullable<Delta<T[k]>>
}
type DeltaObject<T extends {}> = {
[k in keyof T]: Nullable<Delta<T[k]>>
}
type ConditionalEffectCallback = () => CleanupCallback | void
type CleanupCallback = () => void
type Nullable<T> = T | null;
type Optional<T> = T | undefined;
MIT © malerba118