React – Can you update an Unstated container state from an external function?

All we need is an easy explanation of the problem, so here it is.

In the example from the Unstated library they update state within a container by interacting with a jsx button that is subscribed to the container.

import React from 'react';
import { render } from 'react-dom';
import { Provider, Subscribe, Container } from 'unstated';

type CounterState = {
  count: number
};

class CounterContainer extends Container<CounterState> {
  state = {
    count: 0
  };

  increment() {
    this.setState({ count: this.state.count + 1 });
  }

  decrement() {
    this.setState({ count: this.state.count - 1 });
  }
}

function Counter() {
  return (
    <Subscribe to={[CounterContainer]}>
      {counter => (
        <div>
          <button onClick={() => counter.decrement()}>-</button>
          <span>{counter.state.count}</span>
          <button onClick={() => counter.increment()}>+</button>
        </div>
      )}
    </Subscribe>
  );
}

render(
  <Provider>
    <Counter />
  </Provider>,
  document.getElementById('root')
);

Is there a way to update the state within the container from a function in a component outside of the Container? So for instance if I want up to update state during the return of a promise how would I go about doing so. Pseudo code

login = () => {
    let url = baseURL + '/user/login?_format=json';  

    let data = {
      "name": this.state.email,  
      "pass": this.state.password
    };



        axios({
          url,
          method: "POST",
          headers: {
            'Accept':  'application/json',
            'Content-Type': 'application/json',
          },
          withCredentials: true,
          credentials: 'same-origin', 
          data,
          })
          .then(function(result) {
            console.log('result:', result);
                SET STATE HERE!!!!!!!
counter.increment()

              })
          .catch(error => console.log('error:', error));
      };

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

The problem isn’t specific to Unstated, this would apply to React context API that uses render prop pattern, too.

The problem with login is that its control flow is flawed. It isn’t be able to efficiently catch errors because it suppresses them. And it encapsulates the promise inside, which is a mistake that prevents it from being properly tested, for starters

It can expose a promise, or accept a callback, or both:

login = async (cb) => {
  ...    
  return axios(...)
  .then(function(result) {
    if (cb)
      cb(result);

    return result;
  })
  .catch(error => console.log('error:', error));
}

Can be used as:

<button onClick={() => login(() => counter.decrement())}>-</button>

Or:

<button onClick={async () => { await login(); counter.decrement(); }}>-</button>

It’s also possible to make login accept counter as an argument but this would couple it to implementation which is unnecessary.

Method 2

This is a common question, with many solutions to your problem. You need to pass the function to a child, that then updates the parent. I will link to a previous answer that I posted. In your case, where you render

<Login upParentCounter={this.increment}/>

And inside the login component

this.props.upParentCounter()

Dealing with Nested React Components' State Changes

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply