Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useCallback() invalidates too often in practice #14099

Open
gaearon opened this issue Nov 5, 2018 · 71 comments
Open

useCallback() invalidates too often in practice #14099

gaearon opened this issue Nov 5, 2018 · 71 comments

Comments

@gaearon
Copy link
Member

@gaearon gaearon commented Nov 5, 2018

This is related to #14092, #14066, reactjs/rfcs#83, and some other issues.

The problem is that we often want to avoid invalidating a callback (e.g. to preserve shallow equality below or to avoid re-subscriptions in the effects). But if it depends on props or state, it's likely it'll invalidate too often. See #14092 (comment) for current workarounds.

useReducer doesn't suffer from this because the reducer is evaluated directly in the render phase. @sebmarkbage had an idea about giving useCallback similar semantics but it'll likely require complex implementation work. Seems like we'd have to do something like this though.

I'm filing this just to acknowledge the issue exists, and to track further work on this.

@edkalina
Copy link

@edkalina edkalina commented Nov 7, 2018

@gaearon thank you for your answer in reactjs/rfcs#83. I've look at sources of useReducer. But I can't understand how it is related to useCallback. What issues has "mutation of ref during rendering"? Can you explain me in brief?

Loading

@gaearon
Copy link
Member Author

@gaearon gaearon commented Nov 7, 2018

I've look at sources of useReducer. But I can't understand how it is related to useCallback

useCallback lets you memoize the callback to avoid a different function being passed down every time. But you have to specify everything it depends on in the second array argument. If it's something from props or state, your callback might get invalidated too often.

useReducer doesn't suffer from this issue. The dispatch function it gives you will stay the same between re-renders even if the reducer itself closes over props and state. This works because the reducer runs during the next render (and thus has natural ability to read props and state). It would be nice if useCallback could also do something like this but it's not clear how.

What issues has "mutation of ref during rendering"? Can you explain me in brief?

In concurrent mode (not yet released), it would "remember" the last rendered version, which isn't great if we render different work-in-progress priorities. So it's not "async safe".

Loading

@strayiker
Copy link

@strayiker strayiker commented Nov 10, 2018

Would be nice if second argument of useCallback was injected as dependencies to callback function.

  function useCallback(cb, deps) => {
    lastDeps = deps; // save current deps and cb deep in somewhere
    lastCb = cb;

    if (!cached) {
      cached = (...args) => lastCb(...lastDeps)(...args); // memoize that forevere
    }

    return cached; // never invalidates
  }

  const myCallback = useCallback(
    (state, props) => (a, b) => a + b + state + props,
    [state, props]
  );

  myCallback(1, 2)

Loading

@sokra
Copy link

@sokra sokra commented Nov 19, 2018

const useCallback = (fn, args) => {
  const callback = useMemo(() => {
    if (__DEV__) {
      if (fn.length !== args.length) warning(...);
    }
    const callback = () => fn(...callback.args);
    return callback;
  });
  useEffect(() => callback.args = args, [args]);
  return callback;
}

Drawbacks:

It's easy to forget the arguments list, which would result in hard to find bugs. In dev mode it would make sense to check fn.length for the correct length.

It's still possible to forget arguments in the dependencies array, but this applies to other hooks too.

Loading

@gaearon
Copy link
Member Author

@gaearon gaearon commented Nov 19, 2018

Yes, that's the approach from reactjs/rfcs#83 and https://reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback. We don't want it to be default because it's easier to introduce bugs in concurrent mode this way.

Loading

@sophiebits
Copy link
Collaborator

@sophiebits sophiebits commented Nov 19, 2018

@sokra An alternate would be:

function useEventCallback(fn) {
  let ref = useRef();
  useLayoutEffect(() => {
    ref.current = fn;
  });
  return useCallback(() => (0, ref.current)(), []);
}

This doesn't require the args like yours has. But again, you can't call this in the render phase and the use of mutation is dicey for concurrent.

Loading

@sokra
Copy link

@sokra sokra commented Nov 20, 2018

@sophiebits That's clever and would have none of the problems with args, etc. It even doesn't require a dependencies list.

One nitpick: return useCallback((...args) => (0, ref.current)(...args), []); to pass along i. e. event argument.

Loading

@strayiker
Copy link

@strayiker strayiker commented Nov 20, 2018

@sokra With this you will not be able to access to state and props updates inside a callback.

const [state, setState] = useState(0);

const handleClick = useCallback((event) => {
  console.log(state); // always 0
  setState(s => s + 1);
});

return <button onClick={handleClick} />

So dependencies are required.

function useCallback(fn, deps) {
  const fnRef = useRef(fn);
  const depsRef = useRef(deps);

  useLayoutEffect(() => {
    fnRef.current = fn;
    depsRef.current = deps;
  });

  return useCallback((...args) => (0, ref.current)(...depsRef.current)(...args), []);
}
cons handleClick = useCallback(
  (state) => event => console.log(state), // up-to-date
  [state]
);

Loading

@sokra
Copy link

@sokra sokra commented Nov 20, 2018

@sokra With this you will not be able to access to state and props updates inside a callback.

I think with @sophiebits' approach this will work. The latest function is always copied into the ref and only a trampoline function is returned. This will make sure that the latest function is called, which has the latest state in context.

Loading

@muqg
Copy link

@muqg muqg commented Nov 21, 2018

I recently made a duplicate issue and was asked to check this one. What I proposed there was very similar to @sophiebits' approach, but still looks a bit simpler to me:

function useStatic(cb) {
  const callback = useRef(cb)
  callback.current = cb

  const mem = useRef((...args) => callback.current(...args))
  return mem.current
  
  // We could, of course still, use the useCallback hook instead of a second reference.
  // return useCallback((...args) => callback.current(...args), [])
  // Although I think that the one above is better since it avoids the need to compare anything at all.
}

This way it is guaranteed to update where the hook is called since it does not directly use any side effect and instead it only updates a reference. It seems to me that it should be callable during the render phase and should not be dicey with concurrent mode (unless references don't meet these two conditions). Wouldn't this approach be a little better or am I missing something?

Loading

@gaearon
Copy link
Member Author

@gaearon gaearon commented Nov 21, 2018

@muqg In concurrent mode, last render doesn't necessarily mean "latest committed state". So a low-priority render with new props or state would overwrite a reference used by current event handler.

Loading

@strayiker
Copy link

@strayiker strayiker commented Nov 21, 2018

If I understand the problem correctly...

What if useCallback will return a special callable object with two different states of an our callback function? The first is a current callback, that will changes on each render. The second is a commited callback, that changes within a commit phase.
By default call to that object will trigger the current value, so it can be used in render phase.
But internally, React will pass the commited value to the dom, which prevents the callback from being modified until the next commit.

function Callback(cb) {
  function callback(...args) {
    return callback.current(...args);
  }

  callback.commited = cb;
  callback.current = cb;
  callback.SPECIAL_MARKER_FOR_REACT = true;

  return callback;
}

function useCallback(cb) {
  const callback = useMemo(() => new Callback(cb), []);

  callback.current = cb;
  
  useLayoutEffect(() => {
    callback.commited = cb;
  });

  return callback;
}
function Component(counter) {
  const handler = useCallback(() => {
    console.log(counter);
  });

  handler(); // call to handler.current

  // pass handler.commited to the dom
  return <button onClick={handler} />
}

Loading

@Volune
Copy link

@Volune Volune commented Nov 22, 2018

I don't think there is a point in saving the "current", if you want to call it during rendering, just save it in advance out of the hook:

const handler = () => {/* do something*/};
const callback = useCallback(handler);
// I can call the current:
handler();

I personally don't see any benefits of the current useCallback implementation over the proposed useEventCallback, will it become the new implementation?
Also, can it warn when the callback is called during render in development mode?

Loading

@strayiker
Copy link

@strayiker strayiker commented Nov 22, 2018

Concurrent mode can produce two different representations of a component (the first one is that commited to the dom and the second one is that in memory). This representations should behaves accordingly with their props and state.

useEventCallback by @sophiebits mutates ref.current after all dom mutations is completed, so the current (in-memory) component can't use the newest callback until the commit is done.

@muqg proposal mutate the callback on each render, so the commited component will lose the reference to the old callback.

The point of my proposal in the passing a separated callback reference, that will changes in commit phase, to the dom, while the in-memory (not commited) representation of a component can use the latest version of that callback.

const handler = () => {/* do something*/};
const callback = useCallback(handler);

In this case, you wont pass down the handler to other components because it always changes. You will pass the callback, but will face again the concurrent mode problem.

Loading

@Voronar
Copy link

@Voronar Voronar commented Dec 14, 2018

Hi.
According to @sophiebits useEventCallback implementation why is it
function uses useLayoutEffect and not useEffect for ref updating?
And is it normal due to current limitations use useEventCallback for all internal regular functions with some logic (which wants be memoized for
using in expensive pure components tree or/and has closured variables from outer hook function) inside custom hook?

Loading

@jonnyasmar
Copy link

@jonnyasmar jonnyasmar commented Dec 29, 2018

How/why is it that variables are dereferenced within useEffect?

Whether or not the effect is called again based on changes to state/reducer/etc (useEffect's second param), shouldn't have any implication on those variable's references within useEffect, right?

This behavior seems unexpected and having to leverage "escape hatches" just feels broken to me.

Loading

@Bazzer588
Copy link

@Bazzer588 Bazzer588 commented Jan 5, 2019

I have a problem with converting this kind of thing to use functional components and the useCallback hook...

export class TestForm extends React.Component {

    onChangeField = (name,value) => {
        this.setState({ [name]: value });
    };

    render () {
        const state = this.state;
        return (
            <>
                <PickField name="gender"      value={state.gender}      onChangeField={this.onChangeField} />
                <TextField name="firstName"   value={state.firstName}   onChangeField={this.onChangeField} />
                <TextField name="lastName"    value={state.lastName}    onChangeField={this.onChangeField} />
                <DateField name="dateOfBirth" value={state.dateOfBirth} onChangeField={this.onChangeField} />
            </>
        );
    }

The PickField, TextField and DateField components can be implemented with React.PureComponent or React.memo(...). Basically they just display an input field and a label - they have their own onChange handler which calls the onChangeField prop passed in. They only redraw if their specific value changes

onChangeField as above works just fine - but if I try this using a functional component for TestForm and useCallback for onChangeField I just can't get it to 'not' redraw everything on a single field change

Loading

@jonnyasmar
Copy link

@jonnyasmar jonnyasmar commented Jan 5, 2019

@Bazzer588 Are you using React.memo on your functional component? What do your attempts using hooks/functional look like? Your problem may or may not be related to this issue; it's hard to tell without seeing your code.

Loading

@Bazzer588
Copy link

@Bazzer588 Bazzer588 commented Jan 6, 2019

Here's the full code - just drop a <TestForm/> or a <HookTestForm/> somewhere on a page

Using a React.Component, only the field being editted does a full render

import React from 'react';
import TextField from "./TextField";

export default class TestForm extends React.Component {

    onChangeField = (name,value) => {
        this.setState({ [name]: value });
    };

    render () {
        const state = this.state || {};
        return (
            <>
                <TextField name="gender"      value={state.gender}      onChangeField={this.onChangeField} />
                <TextField name="firstName"   value={state.firstName}   onChangeField={this.onChangeField} />
                <TextField name="lastName"    value={state.lastName}    onChangeField={this.onChangeField} />
                <TextField name="dateOfBirth" value={state.dateOfBirth} onChangeField={this.onChangeField} />
            </>
        );
    }
}

Using Hooks, all the child elements are re-rendered every time - presumably as onChangeField changes every time the state data changes. Is there some way I can implement onChangeField so it behaves like the above example?

import React, {useState, useCallback} from 'react';
import TextField from "./TextField";

export default React.memo( () => {

    const [data, changeData] = useState({});

    const onChangeField = useCallback((name,value) => {
        changeData({ ...data, [name]: value });
    }, [data] );

    return (
        <>
            <TextField name="gender"      value={data.gender}      onChangeField={onChangeField} />
            <TextField name="firstName"   value={data.firstName}   onChangeField={onChangeField} />
            <TextField name="lastName"    value={data.lastName}    onChangeField={onChangeField} />
            <TextField name="dateOfBirth" value={data.dateOfBirth} onChangeField={onChangeField} />
        </>
    );
});

This is my <TextField> component - you can see when it does a full render from the console or with the React dev tools

import React from 'react';

export default React.memo( ({ name, value, onChangeField }) => {

    console.log('RENDER TEXT FIELD',name,value);

    return (
        <div className="form-field">
            <label htmlFor={name}>{name}</label>
            <input
                type="text"
                onChange={ (ev) => onChangeField( name, ev.target.value ) }
                value={value || ''}
            />
        </div>
    );
});

Loading

@muqg
Copy link

@muqg muqg commented Jan 6, 2019

@Bazzer588 I think its due to the object value kept in state under the variable name of data. I don't know why but objects in state always invalidate memoization and thus your callback onChangeField is a new on on each render thus breaking the memoization of the components you're passing it to.

I've had a similar issue like you and noticed this as being its cause. I have no idea why the object in state does not keep its reference when it has not been explicitly set to a new object.

Loading

@Bazzer588
Copy link

@Bazzer588 Bazzer588 commented Jan 6, 2019

Yes my problem is that the in first example (using React.Component) I can create a onChangeField callback which is bound to this, and never changes during renders

Using the new hook methods I can't seem to replicate the way the existing functionality works,

On the project I'm working on we often do this to have a hierachy of components and state:

    onChangeField = (fieldName,fieldValue) => {
        const newValue = { ...this.props.value, [fieldName]: fieldValue };
        this.props.onChangeField( this.props.name, newValue );
    };

So it passes props down the tree (ie value {})
And uses the callback to send new values up the tree

Loading

@dantman
Copy link
Contributor

@dantman dantman commented Jan 6, 2019

Using the new hook methods I can't seem to replicate the way the existing functionality works,

Use useReducer.

Loading

@jonnyasmar
Copy link

@jonnyasmar jonnyasmar commented Jan 6, 2019

@Bazzer588 The data var your passing to the second parameter of useCallback is going to invalidate every time. useCallback doesn't do a deep comparison. You need to pass in a flat array for it to properly memoize.

Loading

@mariusGundersen
Copy link

@mariusGundersen mariusGundersen commented Jun 3, 2020

I came across this problem when I tried to call one callback from another one, and observed strange effects. A very simplified example:

const MyComponent = props => {
  const [value, setValue] = useState();
  const [copy, setCopy] = useState();

  const createCopy = useCallback(() => {
    setCopy(value);
  }, [value]);

  const callback = useCallback(() => {
    createCopy();
  }, []);

  return (
    <div>
      <input onChange={e => setValue(e.target.value)} />
      <button onClick={callback}>Copy</button>
      <span>{copy}</span>
    </div>
  );
};

This is obviously a simplified and therefore contrived example. It does not behave how I would expect it to.

According to the documentation it is safe to use setCopy from within a callback (or effect) without adding it to the dependency list. But it appears it's not safe to call another callback created with useCallback without adding it to the dependency list. There is probably a good reason for it, but it seems weird to me and clearly several other people in this thread.

So in the above example calling callback() will not update copy, since it calls an old version of createCopy which holds an old version of value.

Loading

@Hypnosphi
Copy link
Contributor

@Hypnosphi Hypnosphi commented Jun 3, 2020

@mariusGundersen setCopy is guaranteed to remain the same through component's lifetime, while createCopy changes each time when value does

Loading

@mariusGundersen
Copy link

@mariusGundersen mariusGundersen commented Jun 3, 2020

Yes, that's clearly how it behaves. But it also means that callback needs to change too. It would be useful and consistent if useCallback behaved like useState.

Loading

@davidnussio
Copy link

@davidnussio davidnussio commented Jun 4, 2020

@mariusGundersen If you want callback change too, it need setCopy as its dependency.
In this way it is guarantee that no side effects happened

Loading

@liorJuice
Copy link

@liorJuice liorJuice commented Jun 10, 2020

I found that you can use useReducer to mitigate this effectively. the bit that was hard to swallow is when i needed side effects to happen in the callback.

Side effects cant happen in the reducer as it is being called with past actions a lot, on re renders, for some reason. so the reducer must be pure (state, action) => resultState, with no side effects.

The solution was to use useEffect that listen to changes in the reduced state (resultState), to initiate side effects, which suddenly seemed obvious. 😅

for example, this code:

  const [counter, setCounter] = useState(0);

  const handleClick = useCallback(() => {
    console.log(`child clicked ${counter + 1} times`);
    setCounter((c) => c + 1);
  }, [counter]);

which will create a very problematic callback, could be perfectly replaced with:

  const [counter, handleClick] = useReducer((c) => c + 1, 0);

  useEffect(() => {
    counter && console.log(`child clicked ${counter} times`);
  }, [counter]);

which will have the same effect but with handler that will persist through the entire lifetime of the component.

i think this is the most complete solution to the useCallback problem.
@gaearon can you confirm?

Loading

@jedwards1211
Copy link
Contributor

@jedwards1211 jedwards1211 commented Jun 10, 2020

@liorJuice I wonder if in some cases people would need side effects to happen synchronously, right away when something is clicked though?

Loading

@Hypnosphi
Copy link
Contributor

@Hypnosphi Hypnosphi commented Jun 10, 2020

@liorJuice how would you use props in side effects with this approach?

Loading

@liorJuice
Copy link

@liorJuice liorJuice commented Jun 14, 2020

@Hypnosphi like other effect dependencies. remember that if if you only use them for calculations - you can safely move them to the reducer.

Loading

@Hypnosphi
Copy link
Contributor

@Hypnosphi Hypnosphi commented Jun 14, 2020

@liorJuice

like other effect dependencies

then the effect will trigger whenever the prop changes which is probably not what you want.

I'm talking about cases like that:

const handleClick = useCallback(() => {
  console.log(props.name);
}, [props.name]);

How would you apply your approach to this case?

Loading

@jedwards1211
Copy link
Contributor

@jedwards1211 jedwards1211 commented Jun 14, 2020

You wouldn't need to pass props used in side effects as dependencies -- the new values for those props would be part of the new closure when the effect fires because the counter dependency changed.

  useEffect(() => {
    counter && console.log(`${props.name}: child clicked ${counter} times`);
  }, [counter]);

Though I assume it may be possible for the props to change between the click that incremented the counter and the render that triggers the effect?

Loading

@liorJuice
Copy link

@liorJuice liorJuice commented Jun 14, 2020

@Hypnosphi the problem you describe is a general useEffect problem (how to write effects with dependencies that don't need to trigger the effect),
it is not specific to the the useCallback solution i described.
there are number of approaches you can take to solve prop dependencies in effects.

i suggest taking a look here:
https://overreacted.io/a-complete-guide-to-useeffect/#why-usereducer-is-the-cheat-mode-of-hooks

there is also a custom hook named "usePrevious", you can use it to check if a specific dependency changed on useEffect. im not sure how much of a best practice it is.

here is one solution:

  const [{logReport}, handleClick] = useReducer((state) => ({
    counter: state.counter + 1,
    logReport: `${props.name}: child clicked ${state.counter + 1} times`
  }), {counter: 0, logReport: ''});

  useEffect(() => {
    logReport && console.log(logReport);
  }, [logReport]);

you can use props inside reducers if you put the reducer function inside the component (which you shouldn't do by default, but you can).
note that logReport does not change when props.name changes, but only when the user clicks.

Loading

@nikparo
Copy link

@nikparo nikparo commented Jun 30, 2020

I was inspired by @liorJuice's thoughts on useReducer -> useEffect above to create a custom hook as an experiment, use-effect-callbacks. It transforms an object of callbacks into stable methods that can then be passed down. It works by caching the callbacks through a useReducer and executing them in the next useEffect. Could be used as such:

import { useEffectCallbacks } from 'use-effect-callbacks';

// ...

// Within parent component
const [state, setState] = useState({ foo: 'bar' });
const { getState } = useEffectCallbacks({
  getState: () => state,
});

// ...

// Within memoized child component
const handleClick = async () => {
  setState({ foo: 'baz' });
  const latestState = await getState();
  console.log(latestState); // -> { foo: 'baz' }
};

Pros:

  • It should be concurrent safe.
  • The methods are stable as long as the callback names are stable.
  • It is quite straightforward and simple to use.
  • You don't need to manually keep track of a lot of different properties in your state to trigger various effects. (Compared to the more manual approach proposed by @liorJuice.)

Cons:

  • It's a bit wasteful.
    • Calling a method will trigger the owner component to re-render. (Not a problem if you expect it to re-render anyway, e.g. due to a setState call)
    • If a method is called in a useEffect, I believe it will still trigger a full render-cycle and only execute in the next useEffect.
  • The methods are not suitable as event listeners since they execute later, and any react event would have been recycled. (They can however very well be called from event listeners)
  • While the methods can "safely" be called during renders, there's not much point in doing so since they return promises.

Conclusion:

To be perfectly honest I'm still not quite sure what to think of this approach. It seems handy for occasionally getting the next state, but most other use-cases I can think of seem like a bit of a stretch.

Loading

@ypresto
Copy link

@ypresto ypresto commented Jul 16, 2020

FYI: My snippet of useImperativeHandle solution #14099 (comment) is like this (it is TypeScript, remove type annotations for JS):

// Better useCallback() which always returns the same (wrapped) function reference and does not require deps array.
// Use this when the callback requires to be same ref across rendering (for performance) but parent could pass a callback without useCallback().
// useEventCallback(): https://github.com/facebook/react/issues/14099#issuecomment-499781277
// WARNING: Returned callback should not be called from useLayoutEffect(). https://github.com/facebook/react/issues/14099#issuecomment-569044797
export function useStableCallback<T extends (...args: any[]) => any>(fn: T): T {
  const ref = useRef<T>()
  useImperativeHandle(ref, () => fn) // Assign fn to ref.current (currentFunc) in async-safe way

  return useRef(((...args: any[]) => {
    const currentFunc = ref.current
    if (!currentFunc) {
      throw new Error('Callback retrieved from useStableCallback() cannot be called from useLayoutEffect().')
    }
    return currentFunc(...args)
  }) as T).current
}

Loading

@dcheung2
Copy link

@dcheung2 dcheung2 commented Jan 19, 2021

my workaround try to be KISS by abuse self reference from JS context.

But it always point to latest, not sure what could it break in various mode and lifecycle in react.
At least it work in my case.

export default function useStableCallback<T extends (...args: any[]) => unknown>(callback: T): T {
  const ref = useRef({
    stableProxy: (...args: any) => {
      return ref.current.targetFunc(...args);
    },
    targetFunc: callback,
  });
  ref.current.targetFunc = callback;
  return ref.current.stableProxy as T;
}

Loading

@Gentlee
Copy link

@Gentlee Gentlee commented Mar 12, 2021

I got problem with invalidated callback when try to call it after some state mutation.

Very simplified example:

const onClick = (value) => {
    setValue(value);
    navigateToNextPage();
}

Here navigateToNextPage uses old state in its closure and leads to a wrong screen.

I had to create useNextUpdateCallback hook to make it work on the next render:

const navigateOnNextUpdate = useNextUpdateCallback(navigateToNextPage);

const onClick = () => {
    setValue(value);
    navigateOnNextUpdate();
}

Implementation:

const forceUpdateReducer = (x) => x + 1;

export const useForceUpdate = () => {
  const [, forceUpdate] = useReducer(forceUpdateReducer, 0);
  return forceUpdate;
};

export const useNextUpdateCallback = (callback) => {
  // eslint-disable-next-line no-unused-vars
  const forceUpdate = useForceUpdate();
  const shouldCallRef = useRef(false); // ref is used not to rerender after callback is called

  useEffect(() => {
    if (!shouldCallRef.current) return;

    callback();
    shouldCallRef.current = false;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [shouldCallRef.current]);

  return useCallback(() => {
    shouldCallRef.current = true;
    forceUpdate();
  }, [forceUpdate]);
};

Problems:

  • Navigation happens not immediately, this feels a little slower.
  • It is a hack.

Does anyone know a better workaround?

PS. The more I use React and hooks, the more i understand how complex they are. Too complex.

Loading

@vdgodse
Copy link

@vdgodse vdgodse commented May 21, 2021

@Gentlee I ran into a different problem but the workaround should help in your case as well.
Let's assume navigateToNextPage depends on state currentPage in the closure. You could modify navigateToNextPage to read currentPage from an argument rather than the state in the closure.

const onClick = () => {
    setValue(value);
    setCurrentPage((currentPage)=>{
         navigateToNextPage(currentPage)
         return currentPage
    });
}

Caution: It does not work in all cases, eg: if this setState call causes parent component to re-render.

Loading

@jedwards1211
Copy link
Contributor

@jedwards1211 jedwards1211 commented Jun 8, 2021

Hoping there will be some solution to this in React 18.

What if we had some kind of ref hook that safely keeps track of the last commited value on any given render thread?

function useEventCallback(fn) {
  let ref = React.useConcurrentRef(fn);
  // value could be updated like:
  //   ref.setCurrent(fn);
  return useCallback((...args) => (0, ref.getCurrent)(...args), []);
}

Maybe it could have a current property with getters/setters just like useRef; I just used setCurrent/getCurrent in my example to illustrate that magic would be happening under the hood.

Loading

@windmaomao
Copy link

@windmaomao windmaomao commented Jun 18, 2021

I smell useRef here. Either you want to manage the update or not.

Loading

zroyer added a commit to zroyer/lerna-shipwell that referenced this issue Oct 1, 2021
…inting rules (#191)

* Add exhaustive-deps to our list of enforced linting rules

* Pass mutable dependencies to useCallback in file selector

* Pass mutable prop to useEffect dependencies in text input forwarded ref test

* Use a ref for a the timeout handle that needs to persist over time

This is definitely the largest change in the PR. I just followed the
guidance of the new linting rule, and all of this makes sense. I
actually think this change might have fixed a bug in our toast timeouts.

* Pass mutable dependencies to useEffect + internalize timer handle so it doesn't get lost on re-render

* No need to pass state setters in dependency arrays

* Don't cause infinite render loops

It took me way too long to track this one down.

When passing a prop as an arrow function, it creates a new prop every
render. We pass the `onClose` prop to the Toast component as an arrow
function quite often. Before, when we were "lying" to React by not
declaring a dependency on `onClose` by the useEffect hook to close the
Toast on a timer, this didn't matter because the hook would only run
when the `show` prop changed. This had a potential bug if `onClose`
ever _did_ change: it could call an old version of `onClose` and result
in setting some stale state.

After including the true dependencies of useEffect, including the close
handler that called `onClose` an infinite render loop came into play.

- Toast renders with an `onClose` prop and `show` is false.
- The Toast useEffect hook runs, and (indirectly) calls `onClose`.
- `onClose` (indirectly) causes  the `show` prop to be set to false.
- This shouldn't have triggered a re-render, because `show` has the same
  value as last render, but it re-renders anyway because the `onClose`
  prop has changed – it's a new arrow function!
- Toast renders with a "new" `onClose` prop and `show` is false.
- ad infinitum.

Simply not calling the close handler when the Toast is not being shown
was the simple solution to this issue. If for some reason we _did_ need
to call a prop that is a function on every update, there are ways around
it.

facebook/react#14099 (comment)

This hook stores the callback in a ref and calls _that_ in useCallback,
bypassing the need for dependencies to useCallback but still avoiding
the initial bug of a possibly-stale prop.
zroyer added a commit to zroyer/lerna-shipwell that referenced this issue Oct 1, 2021
# [1.2.0](shipwell/shipwell-ui@v1.1.0...v1.2.0) (2020-09-30)

### Bug Fixes

* handle non-string errors ([9ed3604](shipwell/shipwell-ui@9ed3604))
* remove white space styling ([#95](shipwell/shipwell-ui#95)) ([8b9c57e](shipwell/shipwell-ui@8b9c57e))
* uppercase labels when active and adjust margins on no-label fields ([#97](shipwell/shipwell-ui#97)) ([7827d73](shipwell/shipwell-ui@7827d73))
* **build:** Explicitly tell docz to use babelrc ([#127](shipwell/shipwell-ui#127)) ([64ade67](shipwell/shipwell-ui@64ade67))
* **build:** Target actual browsers when building ([#224](shipwell/shipwell-ui#224)) ([73a348f](shipwell/shipwell-ui@73a348f))
* **button:** adds disabled styling to icon button ([#177](shipwell/shipwell-ui#177)) ([cb5c6e5](shipwell/shipwell-ui@cb5c6e5))
* **button:** Button icon alignment ([#203](shipwell/shipwell-ui#203)) ([8ae7a1d](shipwell/shipwell-ui@8ae7a1d))
* **button:** ignore icon buttons when setting min width ([#149](shipwell/shipwell-ui#149)) ([14b340a](shipwell/shipwell-ui@14b340a))
* **button:** More flexible activeIcon variant ([#213](shipwell/shipwell-ui#213)) ([8928de9](shipwell/shipwell-ui@8928de9))
* **button:** shrink font size on small variant ([#172](shipwell/shipwell-ui#172)) ([c2aa859](shipwell/shipwell-ui@c2aa859))
* **button:** specifies disabled state for warning variant ([#180](shipwell/shipwell-ui#180)) ([c3fd517](shipwell/shipwell-ui@c3fd517))
* **button:** update styling for icon buttons ([#164](shipwell/shipwell-ui#164)) ([4ebdfaf](shipwell/shipwell-ui@4ebdfaf))
* **button:** updates Secondary and Book Now button variant styles ([#160](shipwell/shipwell-ui#160)) ([49d8608](shipwell/shipwell-ui@49d8608))
* **button:** Use default line height for buttons ([#226](shipwell/shipwell-ui#226)) ([5e86336](shipwell/shipwell-ui@5e86336))
* **card:** reduces card header min-height ([bfc9b96](shipwell/shipwell-ui@bfc9b96))
* **checkbox:** add indeterminate style  ([3d35797](shipwell/shipwell-ui@3d35797))
* **checkbox:** make disabled checkbox color darker ([#65](shipwell/shipwell-ui#65)) ([fbbc87a](shipwell/shipwell-ui@fbbc87a))
* **colors:** component color refresh ([a9be981](shipwell/shipwell-ui@a9be981))
* **colors:** deprecates $well-water-opacity ([#168](shipwell/shipwell-ui#168)) ([2c5b633](shipwell/shipwell-ui@2c5b633))
* **datepicker:** Ensure time picker breaks words properly in all contexts ([#212](shipwell/shipwell-ui#212)) ([22c9882](shipwell/shipwell-ui@22c9882))
* **datepicker:** Make DatePicker timezone-aware by default ([#215](shipwell/shipwell-ui#215)) ([0f9e914](shipwell/shipwell-ui@0f9e914))
* **datepicker:** pin react-datepicker due to styling bug ([#62](shipwell/shipwell-ui#62)) ([8fc0c51](shipwell/shipwell-ui@8fc0c51))
* **datepicker:** update z index ([d0bd300](shipwell/shipwell-ui@d0bd300))
* **datePicker:** Make Date Picker work with docz again ([#218](shipwell/shipwell-ui#218)) ([50bcc9a](shipwell/shipwell-ui@50bcc9a))
* **datePicker:** tests ([99a6394](shipwell/shipwell-ui@99a6394))
* **DatePicker:** Trigger onChange for Formik version of the date picker ([#233](shipwell/shipwell-ui#233)) ([569ce12](shipwell/shipwell-ui@569ce12))
* **dateTimePicker:** modify UTC parsing ([#57](shipwell/shipwell-ui#57)) ([7a39120](shipwell/shipwell-ui@7a39120))
* **dependencies:** Fix some npm install warnings ([#205](shipwell/shipwell-ui#205)) ([4636736](shipwell/shipwell-ui@4636736))
* **dndContainer:** placeholder ([#139](shipwell/shipwell-ui#139)) ([9618d21](shipwell/shipwell-ui@9618d21))
* **dropdown:** adds prop to control indicator ([e2ffbf4](shipwell/shipwell-ui@e2ffbf4))
* **dropdown:** functional updates for menu toggling ([b42a859](shipwell/shipwell-ui@b42a859))
* **dropdown:** Margin for dropdown menus in line with our usual spacing increments ([#189](shipwell/shipwell-ui#189)) ([3faf83e](shipwell/shipwell-ui@3faf83e))
* **error:** add class to error display for scrolling ([d8bab00](shipwell/shipwell-ui@d8bab00))
* **fileselect:** upload PDFs correctly ([0dcd7a1](shipwell/shipwell-ui@0dcd7a1))
* **fileSelect:** add link to remove selected file ([#126](shipwell/shipwell-ui#126)) ([a6b05df](shipwell/shipwell-ui@a6b05df))
* **fileSelect:** Fixes grid issue with variable grid items ([7340e1c](shipwell/shipwell-ui@7340e1c))
* **fileSelect:** fixes grid issue with variable items ([7600ae2](shipwell/shipwell-ui@7600ae2))
* **fileSelect:** fixes grid issue with variable items ([43dbd22](shipwell/shipwell-ui@43dbd22))
* **fileSelect:** fixes grid issue with variable items ([b2ddf16](shipwell/shipwell-ui@b2ddf16))
* **fileSelect:** handle clicking download, font color consistency ([#150](shipwell/shipwell-ui#150)) ([603f932](shipwell/shipwell-ui@603f932))
* **fileSelect:** update styling for empty input and initial file display ([#79](shipwell/shipwell-ui#79)) ([5a7ec10](shipwell/shipwell-ui@5a7ec10))
* **formGroup:** disabled styles ([#155](shipwell/shipwell-ui#155)) ([9d2848b](shipwell/shipwell-ui@9d2848b))
* **formGroup:** handle array of errors in formGroup ([#91](shipwell/shipwell-ui#91)) ([c5b51bb](shipwell/shipwell-ui@c5b51bb))
* **hooks:** Add react-hooks/exhaustive-deps to our list of enforced linting rules ([#191](shipwell/shipwell-ui#191)) ([858a35a](shipwell/shipwell-ui@858a35a)), closes [/github.com/facebook/react/issues/14099#issuecomment-440013892](https://github.com//github.com/facebook/react/issues/14099/issues/issuecomment-440013892)
* **imageSelect:** adds isAvatar prop to control shape of image ([775cf63](shipwell/shipwell-ui@775cf63))
* **imageSelect:** increase height of parent to ensure slider is always visible when editing image ([8ba2a5c](shipwell/shipwell-ui@8ba2a5c))
* **ImageSelector:** add updated image prop and capitalize ImageActionContainer ([#131](shipwell/shipwell-ui#131)) ([8bcaa51](shipwell/shipwell-ui@8bcaa51))
* **inputGroup:** fix icon height ([23369bf](shipwell/shipwell-ui@23369bf))
* **labelInput:** fixes blur functionality ([#195](shipwell/shipwell-ui#195)) ([9b9e66d](shipwell/shipwell-ui@9b9e66d))
* **Menu:** Menu z-index shouldn't be higher than modal overlay ([#179](shipwell/shipwell-ui#179)) ([bdac650](shipwell/shipwell-ui@bdac650))
* **misc:** add optional chaining to babel ([821c0d0](shipwell/shipwell-ui@821c0d0))
* **misc:** adds optional chaining to babel ([a7d21c1](shipwell/shipwell-ui@a7d21c1))
* **misc:** adjust modal heights, fix phone number country picker ([#56](shipwell/shipwell-ui#56)) ([a9cb7e4](shipwell/shipwell-ui@a9cb7e4))
* **misc:** misc bug and style fixes ([7aa35c8](shipwell/shipwell-ui@7aa35c8))
* **misc:** misc bug and style fixes ([#54](shipwell/shipwell-ui#54)) ([57ebef1](shipwell/shipwell-ui@57ebef1))
* **misc:** misc style and functional updates ([#44](shipwell/shipwell-ui#44)) ([cf52c41](shipwell/shipwell-ui@cf52c41))
* **misc:** more misc style and bug fixes ([#55](shipwell/shipwell-ui#55)) ([42972e8](shipwell/shipwell-ui@42972e8))
* **modal:** adjust modal styles ([#94](shipwell/shipwell-ui#94)) ([8b74a9e](shipwell/shipwell-ui@8b74a9e))
* **modal:** deprecates warningSecondary variant ([40f7db2](shipwell/shipwell-ui@40f7db2))
* **modal:** fixes body overflow styling ([#157](shipwell/shipwell-ui#157)) ([781ede2](shipwell/shipwell-ui@781ede2))
* **modal:** sets modal body white-space ([#181](shipwell/shipwell-ui#181)) ([e9f1413](shipwell/shipwell-ui@e9f1413))
* **noop:** trigger release ([a3581fd](shipwell/shipwell-ui@a3581fd))
* **pill:** Use design guidelines for pill spacing ([#229](shipwell/shipwell-ui#229)) ([6c85d5c](shipwell/shipwell-ui@6c85d5c))
* **radioGroup:** docz navigation fix ([#221](shipwell/shipwell-ui#221)) ([2e4bf17](shipwell/shipwell-ui@2e4bf17))
* **release:** Create releases in `latest` channel from dev branch ([#235](shipwell/shipwell-ui#235)) ([39791d1](shipwell/shipwell-ui@39791d1))
* **release:** Release and deploy latest shipwell-ui ([9b07afd](shipwell/shipwell-ui@9b07afd))
* **release:** Use latest/existing semantic-release binary to do the release ([#225](shipwell/shipwell-ui#225)) ([826d016](shipwell/shipwell-ui@826d016))
* **searchbar:** export searchbar component ([43c89fc](shipwell/shipwell-ui@43c89fc))
* **Searchbar:** enable component to use typeahead field ([#75](shipwell/shipwell-ui#75)) ([12a265b](shipwell/shipwell-ui@12a265b))
* **select:** add max height to select menu list ([#70](shipwell/shipwell-ui#70)) ([47814f3](shipwell/shipwell-ui@47814f3))
* **select:** adds margin bottom beneath select component ([#109](shipwell/shipwell-ui#109)) ([91f0699](shipwell/shipwell-ui@91f0699))
* **select:** adds margin bottom for select form fields only ([#110](shipwell/shipwell-ui#110)) ([942b6a9](shipwell/shipwell-ui@942b6a9))
* **select:** check for value existence before accessing length property ([694f54a](shipwell/shipwell-ui@694f54a))
* **select:** Export CollapsibleGroupSelect so we can actually use it ([#210](shipwell/shipwell-ui#210)) ([c9a5b9b](shipwell/shipwell-ui@c9a5b9b))
* **select:** Fix typo around has-error class in select component ([#232](shipwell/shipwell-ui#232)) ([d2aaf0a](shipwell/shipwell-ui@d2aaf0a))
* **select:** fixes multi-select error styling ([#156](shipwell/shipwell-ui#156)) ([7f4710c](shipwell/shipwell-ui@7f4710c))
* **select:** prevents clearing single multivalue option ([4474680](shipwell/shipwell-ui@4474680))
* **select:** select component now handles setting of simpleValue option prop ([64fa447](shipwell/shipwell-ui@64fa447))
* **select:** set touched on blur ([#82](shipwell/shipwell-ui#82)) ([f14419d](shipwell/shipwell-ui@f14419d))
* **select:** update handleChange for simpleValue, other req changes ([aa87d1f](shipwell/shipwell-ui@aa87d1f))
* **sidebar:** Default z-index ([31f1335](shipwell/shipwell-ui@31f1335))
* **svgIcon:** adds svgr ([#106](shipwell/shipwell-ui#106)) ([63f8a6d](shipwell/shipwell-ui@63f8a6d))
* **svgIcon:** makes svg files scalable ([#148](shipwell/shipwell-ui#148)) ([c90d76e](shipwell/shipwell-ui@c90d76e))
* **svgIcon:** pulls colors from the .scss ([#159](shipwell/shipwell-ui#159)) ([e323c0d](shipwell/shipwell-ui@e323c0d))
* **svgIcon:** use correct docz loader ([#107](shipwell/shipwell-ui#107)) ([287ccf9](shipwell/shipwell-ui@287ccf9))
* **textarea:** textarea label overlap ([0116517](shipwell/shipwell-ui@0116517))
* **textArea:** requires name prop ([#115](shipwell/shipwell-ui#115)) ([77c8121](shipwell/shipwell-ui@77c8121))
* **title:** Use flex-grow, not width: 100% to make Title full-width ([#201](shipwell/shipwell-ui#201)) ([49b118e](shipwell/shipwell-ui@49b118e))
* **toast:** manually close toast if `show` set to false ([#135](shipwell/shipwell-ui#135)) ([857cad3](shipwell/shipwell-ui@857cad3))
* **toggle:** Allow external and internal state to drive toggle switch ([#200](shipwell/shipwell-ui#200)) ([8ffb236](shipwell/shipwell-ui@8ffb236))
* **toggle:** Toggle switch styles ([#202](shipwell/shipwell-ui#202)) ([51a1663](shipwell/shipwell-ui@51a1663))
* **toggleSwitch:** fixes toggles that arent clicked from turning on ([af8c516](shipwell/shipwell-ui@af8c516))
* **tooltip:** adds wrapper classname ([5cd0e8a](shipwell/shipwell-ui@5cd0e8a))
* **tooltip:** correct typo in export name, update proptypes ([#76](shipwell/shipwell-ui#76)) ([ebdf22a](shipwell/shipwell-ui@ebdf22a))
* **tooltip:** fixes arrow placement ([#114](shipwell/shipwell-ui#114)) ([ba1fd09](shipwell/shipwell-ui@ba1fd09))
* **tooltip:** fixes sw-tooltip background changing on hover ([cf1ed8a](shipwell/shipwell-ui@cf1ed8a))
* **tooltip:** handle click away to close tooltip ([#142](shipwell/shipwell-ui#142)) ([d949ae1](shipwell/shipwell-ui@d949ae1))
* **tooltip:** updates innerRef prop ([fa4e989](shipwell/shipwell-ui@fa4e989))
* **twoColumn:** fixes grid layout ([#146](shipwell/shipwell-ui#146)) ([0e3f161](shipwell/shipwell-ui@0e3f161))
* **typeahead:** add active class selector in typeahead options ([#113](shipwell/shipwell-ui#113)) ([668cc0e](shipwell/shipwell-ui@668cc0e))
* **typeahead:** adds onBlur prop ([b7b12ba](shipwell/shipwell-ui@b7b12ba))
* **typeahead:** hides options on blur ([#102](shipwell/shipwell-ui#102)) ([5b564fb](shipwell/shipwell-ui@5b564fb))
* **typeahead:** ignore onBlur when clicking on menu/option ([28f8531](shipwell/shipwell-ui@28f8531))

### Features

* success validation ([b2b1f88](shipwell/shipwell-ui@b2b1f88))
* **button:** add small and medium sizes ([#145](shipwell/shipwell-ui#145)) ([6295e18](shipwell/shipwell-ui@6295e18))
* **button:** adds loading prop ([#216](shipwell/shipwell-ui#216)) ([8938827](shipwell/shipwell-ui@8938827))
* **Button:** Add a very simple text variant for Button ([#171](shipwell/shipwell-ui#171)) ([13b6063](shipwell/shipwell-ui@13b6063))
* **button+card-title:** ActiveIcon button variant and improved Card title   ([#196](shipwell/shipwell-ui#196)) ([e6a1e07](shipwell/shipwell-ui@e6a1e07))
* **card:** add tooltip option for cards ([476152a](shipwell/shipwell-ui@476152a))
* **card:** adds bodyClassName prop ([c01b933](shipwell/shipwell-ui@c01b933))
* **card:** adds collapsible functionality ([#119](shipwell/shipwell-ui#119)) ([80754ed](shipwell/shipwell-ui@80754ed))
* **card:** include Card in FCl ([#49](shipwell/shipwell-ui#49)) ([4e07298](shipwell/shipwell-ui@4e07298))
* **Card:** add header class prop ([#117](shipwell/shipwell-ui#117)) ([004ab64](shipwell/shipwell-ui@004ab64))
* **Card:** Collapsible card content autonomy ([#167](shipwell/shipwell-ui#167)) ([4a9895e](shipwell/shipwell-ui@4a9895e))
* **CheckCircleFilledIcon:** change default fill value for CheckCircleFilledIcon ([508784e](shipwell/shipwell-ui@508784e))
* **CheckCircleFilledIcon:** changed default fill for CheckCircleFilledIcon ([9d84488](shipwell/shipwell-ui@9d84488))
* **CheckCircleFilledIcon:** maintain path prop order in svg ([d707a4b](shipwell/shipwell-ui@d707a4b))
* **colors:** adds border-light semantic var ([c8efe36](shipwell/shipwell-ui@c8efe36))
* **colors:** adds documentation ([#101](shipwell/shipwell-ui#101)) ([45e31da](shipwell/shipwell-ui@45e31da))
* **colors:** adds happy-clouds ([ba095c1](shipwell/shipwell-ui@ba095c1))
* **colors:** adds new colors with docz ([#100](shipwell/shipwell-ui#100)) ([868e0ce](shipwell/shipwell-ui@868e0ce))
* **colors:** new semantic variables for small icon and grey text ([#169](shipwell/shipwell-ui#169)) ([b2dbdbc](shipwell/shipwell-ui@b2dbdbc))
* **datepicker:** add date picker into FCL ([e100305](shipwell/shipwell-ui@e100305))
* **datePicker:** adds hideTimezone prop ([#111](shipwell/shipwell-ui#111)) ([5a6a309](shipwell/shipwell-ui@5a6a309))
* **dateRangePicker:** add dateRangePicker component ([15690d0](shipwell/shipwell-ui@15690d0))
* **displayValue:** allow classname, labelClassName as props ([04fc5ae](shipwell/shipwell-ui@04fc5ae))
* **displayValue:** prop encapsulation ([6456eb1](shipwell/shipwell-ui@6456eb1))
* **DragDropContainer:** Drag-and-drop container layout component ([#125](shipwell/shipwell-ui#125)) ([8c88a3b](shipwell/shipwell-ui@8c88a3b))
* **drawer:** include app drawer into library ([#60](shipwell/shipwell-ui#60)) ([fc78066](shipwell/shipwell-ui@fc78066))
* **dropdown:** onToggle prop for dropdown ([#220](shipwell/shipwell-ui#220)) ([e46aaf4](shipwell/shipwell-ui@e46aaf4))
* **empty:** adds semantic color var for section title ([#166](shipwell/shipwell-ui#166)) ([2675a06](shipwell/shipwell-ui@2675a06))
* **fileSelect:** add accepted types option ([48d4640](shipwell/shipwell-ui@48d4640))
* **fileSelect:** adds 1px border if unable to preview file ([d8412c8](shipwell/shipwell-ui@d8412c8))
* **fileSelect:** adds updated file uploader design ([#144](shipwell/shipwell-ui#144)) ([630ef39](shipwell/shipwell-ui@630ef39))
* **fileSelect:** allow disable and adjust disabled styling ([#77](shipwell/shipwell-ui#77)) ([e30c886](shipwell/shipwell-ui@e30c886))
* **flex:** adds Flex Layout component ([74c766e](shipwell/shipwell-ui@74c766e))
* **icon:** add expand_all and collapse_all ([#158](shipwell/shipwell-ui#158)) ([03665cb](shipwell/shipwell-ui@03665cb))
* **icon:** add phone_outlined ([#147](shipwell/shipwell-ui#147)) ([0f3ece8](shipwell/shipwell-ui@0f3ece8))
* **imageselector:** include image selector into FCL ([#46](shipwell/shipwell-ui#46)) ([3a7f09d](shipwell/shipwell-ui@3a7f09d))
* **imageSelector:** adds the new hotness (imageSelector rewrite) ([#122](shipwell/shipwell-ui#122)) ([4a71c95](shipwell/shipwell-ui@4a71c95))
* **ImageSelector:** add design review touch-ups ([#129](shipwell/shipwell-ui#129)) ([1fbae94](shipwell/shipwell-ui@1fbae94))
* **inputgroup:** include input group into FCL ([#52](shipwell/shipwell-ui#52)) ([1621399](shipwell/shipwell-ui@1621399)), closes [#53](shipwell/shipwell-ui#53)
* **LabelInput:** adds LabelInput component ([#188](shipwell/shipwell-ui#188)) ([33bda4e](shipwell/shipwell-ui@33bda4e))
* **lint:** Enforce import/order eslint rule ([#207](shipwell/shipwell-ui#207)) ([1fa2186](shipwell/shipwell-ui@1fa2186))
* **Loader:** add loader component ([#133](shipwell/shipwell-ui#133)) ([fdc4f34](shipwell/shipwell-ui@fdc4f34))
* **modal:** add fullscreen option ([#83](shipwell/shipwell-ui#83)) ([57a174d](shipwell/shipwell-ui@57a174d))
* **modal:** adds PrimaryButtonProps ([#223](shipwell/shipwell-ui#223)) ([1329853](shipwell/shipwell-ui@1329853))
* **modal:** include modal into FCL ([#48](shipwell/shipwell-ui#48)) ([0a32115](shipwell/shipwell-ui@0a32115))
* **phoneInput:** add success icon and message ([#228](shipwell/shipwell-ui#228)) ([8da7a6a](shipwell/shipwell-ui@8da7a6a))
* **pill:** Status color updates for Pill ([#199](shipwell/shipwell-ui#199)) ([1056cec](shipwell/shipwell-ui@1056cec))
* **release:** Bump package.json version number when releasing ([#227](shipwell/shipwell-ui#227)) ([4f929b6](shipwell/shipwell-ui@4f929b6))
* **release:** Publish shipwell-ui with GitHub App credentials ([#231](shipwell/shipwell-ui#231)) ([0ba27be](shipwell/shipwell-ui@0ba27be))
* **select:** Add a Formik version of the collapsible group select component ([#211](shipwell/shipwell-ui#211)) ([760f65f](shipwell/shipwell-ui@760f65f))
* **select:** add createable option ([#73](shipwell/shipwell-ui#73)) ([2c4b16b](shipwell/shipwell-ui@2c4b16b))
* **select:** add optional components prop ([#98](shipwell/shipwell-ui#98)) ([b9de810](shipwell/shipwell-ui@b9de810))
* **select:** Collapsible option groups for Select ([#206](shipwell/shipwell-ui#206)) ([2e29038](shipwell/shipwell-ui@2e29038))
* **sidebar:** Allow class name for sidebar component ([#176](shipwell/shipwell-ui#176)) ([ad585a3](shipwell/shipwell-ui@ad585a3))
* **sidebar:** SHIP-7152 sidebar component ([#132](shipwell/shipwell-ui#132)) ([90c58e2](shipwell/shipwell-ui@90c58e2))
* **sidebar:** Sidebar improvements for shipment details ([#170](shipwell/shipwell-ui#170)) ([5949cfa](shipwell/shipwell-ui@5949cfa))
* **slider:** adds onChange prop to handleChange and styling fix ([#192](shipwell/shipwell-ui#192)) ([c18462f](shipwell/shipwell-ui@c18462f))
* **slider:** adds Slider component with docz ([#105](shipwell/shipwell-ui#105)) ([d464efb](shipwell/shipwell-ui@d464efb)), closes [#80](shipwell/shipwell-ui#80)
* **svgIcon:** Add a Back icon ([#197](shipwell/shipwell-ui#197)) ([e4cddc1](shipwell/shipwell-ui@e4cddc1))
* **svgIcon:** adds svgIcon component with svg icons and docz ([#104](shipwell/shipwell-ui#104)) ([f27b49a](shipwell/shipwell-ui@f27b49a))
* **tabbedContainer:** SHIP-7141 tabbed container ([#123](shipwell/shipwell-ui#123)) ([600a170](shipwell/shipwell-ui@600a170))
* **textInput:** allow initialvalue ([#84](shipwell/shipwell-ui#84)) ([f655955](shipwell/shipwell-ui@f655955))
* **textInput:** Clearable text input ([#187](shipwell/shipwell-ui#187)) ([021c377](shipwell/shipwell-ui@021c377))
* **title:** Allow Title components to accept a className prop ([#209](shipwell/shipwell-ui#209)) ([9ce8c4b](shipwell/shipwell-ui@9ce8c4b))
* **toast:** a toast message component ([#43](shipwell/shipwell-ui#43)) ([f4b1999](shipwell/shipwell-ui@f4b1999))
* **toast:** adds callToAction ([21f51d6](shipwell/shipwell-ui@21f51d6))
* **toast:** misc style updates ([541f283](shipwell/shipwell-ui@541f283))
* **toggleSwitch:** add the toggle switch back to shipwell-ui exports ([f9d838d](shipwell/shipwell-ui@f9d838d))
* **toggleSwitch:** Add toggle switch component ([f6b22f9](shipwell/shipwell-ui@f6b22f9))
* **toggleSwitch:** add toggleSwitch import back ([05f3ea2](shipwell/shipwell-ui@05f3ea2))
* **tooltip:** adds duration prop ([#103](shipwell/shipwell-ui#103)) ([7338f11](shipwell/shipwell-ui@7338f11))
* **tooltip:** adds optional actions to tooltip ([#162](shipwell/shipwell-ui#162)) ([3f4c405](shipwell/shipwell-ui@3f4c405))
* **tooltip:** include tooltip into library ([b850459](shipwell/shipwell-ui@b850459))
* **tooltip:** include tooltip into library ([3741787](shipwell/shipwell-ui@3741787))
* **twoColumn:** adds gridGap prop ([#140](shipwell/shipwell-ui#140)) ([01ddf53](shipwell/shipwell-ui@01ddf53))
* **TwoColumnLayout:** Two-column layout component ([#120](shipwell/shipwell-ui#120)) ([16ff78e](shipwell/shipwell-ui@16ff78e)), closes [#121](shipwell/shipwell-ui#121)
* **typeahead:** include typeahead in FCL ([#51](shipwell/shipwell-ui#51)) ([b3a1817](shipwell/shipwell-ui@b3a1817))
* **typography:** adds Note component and Title formTitle variant ([#182](shipwell/shipwell-ui#182)) ([65593eb](shipwell/shipwell-ui@65593eb))
* **typography:** adds Typography components ([8a2b5c0](shipwell/shipwell-ui@8a2b5c0))

### Reverts

* Revert "Use an access token from the Shipwell GitHub app" ([ea645be](shipwell/shipwell-ui@ea645be))
* Revert "Convert repository url to ssh to match Jenkins checkout (#230)" ([bc0da64](shipwell/shipwell-ui@bc0da64)), closes [#230](shipwell/shipwell-ui#230)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet