Only valid API states vs React reconcilliation

Viewed 22

I model my types for backend API returns to represent only valid (legal) states:

interface ApiInit {
  kind: 'init';
}
interface ApiSuccess<T> {
  kind: 'success';
  data: T;
}
interface ApiFail {
  kind: 'fail';
  errors: string[];
}
interface ApiLoading {
  kind: 'loading';
}

type ApiReturn<T> = ApiSuccess<T> | ApiFail | ApiLoading | ApiInit

However, in the example below, since only ApiSuccess type has data attribute on it, whenever API goes into the success state React seems to treat the data it receives as a completely new array, resetting all the possible internal states the nested components might have had:

import React, { useState, useEffect } from 'react';
import { render } from 'react-dom';

interface ApiInit {
  kind: 'init';
}
interface ApiSuccess<T> {
  kind: 'success';
  data: T;
}
interface ApiFail {
  kind: 'fail';
  errors: string[];
}
interface ApiLoading {
  kind: 'loading';
}

type ApiReturn<T> = ApiSuccess<T> | ApiFail | ApiLoading | ApiInit

type IOrg = { name: string };

const api = {
  getOrgs: async () => ({
    data: [{ name: 'org1' }, { name: 'org2' }],
    errors: [],
  }),
};

const useApi = <T,>(apiFunc) => {
  const [state, setState] = useState<ApiReturn<T>>({kind: 'init'});

  const request = async () => {
    setState({ kind: 'loading' });
    const { data, errors } = await apiFunc();
    if (errors.length) {
      setState({ kind: 'fail', errors });
    } else {
      setState({ kind: 'success', data });
    }
  };
  return { state, request };
};

const Org = ({org}: {org: IOrg}) => {
  const [state, setState] = useState('not updated')
  return (
    <div>
      <div>{org.name}</div>
      <div>{state}</div>
      <button onClick={() => setState('updated')}>Update</button>
    </div>
  )
}

const Orgs = ({orgs}: {orgs: IOrg[]}) => {
  return (
    <div>
      {orgs.map(org => <Org key={org.name} org={org} />)}
    </div>
  )
}

const App = () => {
  const orgsApi = useApi<IOrg[]>(api.getOrgs);
  // const [orgs, setOrgs] = useState<IOrg[]>([])

  useEffect(() => {
    orgsApi.request()
  }, [])

  // useEffect(() => {
  //   orgsApi.state.kind === 'success' && setOrgs(orgsApi.state.data)
  // }, [orgsApi.state])

  return (
    <div>
      <button onClick={orgsApi.request}>Get orgs</button>
      {orgsApi.state.kind === 'success' && <Orgs orgs={orgsApi.state.data} />}
    </div>
  )
};

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

Interactive demo

To reproduce the issue, please open the demo, click some update buttons and then get orgs. You will see that the internal state of each Org component resets to not updated.

Is there any way to keep the types pattern I have (only valid API states) and still make React reconcile components instead of creating new ones? Preferably without introducing any helper state, only using state that useApi hook returns.

0 Answers
Related