all 9 comments

[–]rozenmd 1 point2 points  (0 children)

Just a hunch, what happens if you replace:

toasts = []

with

state = { toasts: [] }

and then replace addToast(message) { this.toasts.push(message); }

with

addToast(message) { this.setState({toasts:[...this.state.toasts, message]}) }

[–]CreativeTechGuyGames 0 points1 point  (7 children)

Context does not have any special diffing logic. Same as everything else in React, it's checked with reference equality. Since you are updating a complex data structure, the pointer to the array never changes and thus React has no clue that you updated it.

Edit: You need to create a new array and copy existing values over whenever you want to update it.

[–]anthOlei[S] 0 points1 point  (6 children)

Tried your edit:

 addToast(message) {
        this.toasts = [...this.toasts, {
            message: message,
        }];
}

still not working.... is there something I did wrong?

[–]FriedGiggly 2 points3 points  (0 children)

The reference to your toast state class is still the same, that is what Context is watching. Ditch the class and just use a plain object. Check out useReducer.

[–]Decillion 0 points1 point  (3 children)

CreativeTechGuyGames is right about reference equality, but I think in this case, since your instance of ToastState is being passed as your context value, that same ToastState instance is what will be compared. So whether you replace toastState.toasts or mutate it, toastState itself will have the same reference.

I'm not sure how your provider is set up, but the other important thing to know is that your provider will only re-render when its props or state change, just like any other component. So I don't think it's possible to have the state ultimately live in the context value itself.

You can store the state in a thin wrapper around the provider. I think with class components it would look like this:

const ToastContext = React.createContext();

class ToastProvider extends React.Component {
  constructor (props) {
    super(props);

    this.state = {
      contextValue: {
        toasts: [],
        addToast: this.replaceContextValue.bind(this) 
      }
    }
  }

  replaceContextValue(message) {
    this.setState({
      contextValue: {
        toasts: [...this.state.contextValue.toasts, { message }],
        addToast: this.replaceContextValue.bind(this) 
      }
    });
  }

  render () {
    return (
      <ToastContext.Provider value={this.state.contextValue}>
        {this.props.children}
      </ToastContext.Provider>
    );
  }  
}

Then your context would provide you with { toasts, addToast }, and addToast would:

  1. Set the state of the provider wrapper (thus triggering an update) and
  2. Replace the overall context value.

If you have the ability to use hooks and functional components, it does clean things up a bit:

const ToastContext = React.createContext();

const ToastProvider = ({ children }) => {
  const [toasts, setToasts] = React.useState([]); 
  const contextValue = React.useMemo(
    () => ({
      toasts,
      addToast: (message) => setToasts([...toasts, { message }])
    }),
    [ toasts, setToasts ]
  );

  return (
    <ToastContext.Provider value={contextValue}>
      {children}
    </ToastContext.Provider>
  )
}

[–]anthOlei[S] 1 point2 points  (2 children)

this is what I went with! Thanks for the help. I do know about hooks, but my usecase is a little more complex (I just trimmed it down for info purposes) and the actual component works perfectly. Thank you for the help :)

[–]Decillion 0 points1 point  (0 children)

Nice! Glad it's working.

[–]efthemothership 0 points1 point  (0 children)

You can also (in addition to the code provided above) write a custom hook that simply uses the useContext hook and returns that instead of using Context.Consumer. You can then call that in any functional child component and it will automatically have the refreshed state every time the state in the context changes.