Skip to content

Latest commit

 

History

History
526 lines (372 loc) · 14.4 KB

README.md

File metadata and controls

526 lines (372 loc) · 14.4 KB

react-delta

Toolbelt for more flexible effects in react

NPM JavaScript Style Guide

Install

npm install --save react-delta

Overview

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.

Motivation

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.

Scenario One

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.

Gross Solution

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]);
}

Cool Solution

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");
      }
    }
  });
}

Alternate Cool Solution

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");
      }
    }
  });
}

Anotha Alternate Cool Solution

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);
}

Scenario Two

You want to log only when both width and height of the window have changed, but not if only one has changed.

No Problem

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));
}

Scenario Three

You want to set up an interval when the component mounts and its callback needs access to data from future renders.

How About This?

import { useLatest } from 'react-delta';

function useIntervalLogger(data) {
  const dataRef = useLatest(data);

  useEffect(() => {
    const id = setInterval(() => {
      console.log(dataRef.current);
    }, 3000);
    return () => clearInterval(id);
  }, []);
}

Demos

See this playground to mess around with react-delta.

usePrevious

usePrevious-demo

useDelta

useDelta-demo

useDeltaObject

useDeltaObject-demo

useDeltaArray

useDeltaArray-demo

API

usePrevious(value)

Gets the value from the previous render of the observed variable.

Signature

usePrevious<T>(value: T): Optional<T>;

Parameters

  • value: required - a variable to observe across renders.

Returns

The value passed to this hook during the previous render or undefined (if the first render).

Usage

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");
      }
    }
  });
}

useLatest(value)

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.

Signature

useLatest<T>(value: T): MutableRefObject<T>;

Parameters

  • value: required - a variable to observe across renders.

Returns

A ref to the value passed to this hook in the most recent render.

Usage

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);
  }, []);
}

useDelta(value, options)

Determines the delta of value between the current and the previous render.

Signature

useDelta<T>(value: T, options?: { deep?: boolean }): Nullable<Delta<T>>;

Parameters

  • 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.

Returns

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.

Usage

import { useDelta, useConditionalEffect } from 'react-delta'

const useFetch = (url) => {
    const delta = useDelta(url)

    useConditionalEffect(() => {
        fetch(url)
    }, !!delta)
}

useDeltaObject(obj, options)

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)).

Signature

useDeltaObject<T extends {}>(obj: T, options?: { deep?: boolean }): DeltaObject<T>;

Parameters

  • 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.

Returns

An object with the same keys as the passed object, but whose values represent the deltas of the passed object's values.

Usage

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
}

useDeltaArray(array, options)

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))).

Signature

useDeltaArray<T extends any[]>(array: T, options?: { deep?: boolean }): DeltaArray<T>;

Parameters

  • 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.

Returns

An array with the same length as the passed array, but whose values represent the deltas of the passed arrays's values.

Usage

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
}

useConditionalEffect(callback, condition)

Runs an effect when the condition is truthy. If the effect returns a cleanup function, the cleanup function will run before the next effect.

Signature

useConditionalEffect(callback: ConditionalEffectCallback, condition: any): void;

Parameters

  • 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.

Returns

This method has no return value.

Usage

import { useConditionalEffect } from 'react-delta'

const useEvenCountLogger = (count) => {

  useConditionalEffect(() => {
    console.log(count)
  }, count % 2 === 0) 

}

useConditionalLayoutEffect(callback, condition)

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.

Signature

useConditionalLayoutEffect(callback: ConditionalLayoutEffectCallback, condition: any): void;

Parameters

  • 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.

Returns

This method has no return value.

Usage

import { useConditionalLayoutEffect } from 'react-delta'

const useEvenCountLogger = (count) => {

  useConditionalLayoutEffect(() => {
    console.log(count)
  }, count % 2 === 0) 

}

some(array)

Indicates whether some value in the array is truthy.

Signature

some(array: any[]): boolean;

Parameters

  • array: required - an array of values that will be coerced to booleans.

Returns

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.

Usage

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;
};

every(array)

Indicates whether every value in the array is truthy.

Signature

every(array: any[]): boolean;

Parameters

  • array: required - an array of values that will be coerced to booleans.

Returns

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.

Usage

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;
};

Type Definitions

Delta

interface Delta<T> {
    prev?: T;
    curr: T;
}

DeltaArray

type DeltaArray<T extends any[]> = {
    [k in keyof T]: Nullable<Delta<T[k]>>
}

DeltaObject

type DeltaObject<T extends {}> = {
    [k in keyof T]: Nullable<Delta<T[k]>>
}

ConditionalEffectCallback

type ConditionalEffectCallback = () => CleanupCallback | void

CleanupCallback

type CleanupCallback = () => void

Nullable

type Nullable<T> = T | null;

Optional

type Optional<T> = T | undefined;

License

MIT © malerba118