reactjsreact-contextunstated

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


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

Solution

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