Niels
Niels

Reputation: 482

How do you handle external state with React Hooks?

I have a mathematical algorithm that I want to keep separated from React. React would be a view to the state within that algorithm, and should not define the way of how the logic is flowing within the algorithm. Also, since it is separated, it's much easier to unit test the algorithm. I have implemented it using class components (simplified):

class ShortestPathRenderer extends React.Component {

  ShortestPath shortestPath;

  public constructor(props) {
     this.shortestPath = new ShortestPath(props.spAlgorithm);
     this.state = { version: this.shortestPath.getVersion() };
  }

  render() {
    ... // Render waypoints from shortestPath
  }

  onComponentDidUpdate(prevProps) {
    if (prevProps.spAlgorithm !== this.props.spAlgorithm) {
      this.shortestPath.updateAlgorithm(this.props.spAlgorithm);
    }
  }

  onComponentWillUnmount() {
    this.shortestPath = undefined;
  }

  onAddWayPoint(x) {
    this.shortestPath.addWayPoint(x);
    // Check if we need to rerender
    this.setState({ version: this.shortestPath.getVersion() });
  }
}

How would I go about this using React hooks? I was thinking about using the useReducer method. However, the shortestPath variable would then be a free variable outside the reducer and the reducer is no longer pure, which I find dirty. So in this case the whole state of the algorithm must be copied with every update on the internal state of the algorithm and a new instance must be returned, which is not efficient (and forces the logic of the algorithm to be the React-way):

class ShortestPath {
  ... 
  addWayPoint(x) {
    // Do something
    return ShortestPath.clone(this);
  }
}

const shortestPathReducer = (state, action) => {
   switch (action.type) { 
      case ADD_WAYPOINT:
        return action.state.shortestPath.addWayPoint(action.x);
   }
}

const shortestPathRenderer = (props) => {
   const [shortestPath, dispatch] = useReducer(shortestPathReducer, new ShortestPath(props.spAlgorithm));

   return ...
}

Upvotes: 1

Views: 1529

Answers (2)

marzelin
marzelin

Reputation: 11600

I'd go with something like this:

const ShortestPathRenderer = (props) => {
  const shortestPath = useMemo(() => new ShortestPath(props.spAlgorithm), []);
  const [version, setVersion] = useState(shortestPath.getVersion());

   useEffect(() => {
     shortestPath.updateAlgorithm(spAlgorithm);
   }, [spAlgorithm]);

  const onAddWayPoint = (x) => {
    shortestPath.addWayPoint(x);
    // Check if we need to rerender
    setVersion(shortestPath.getVersion());
  }

  return (
    ... // Render waypoints from shortestPath
  )
}

you can even decouple logic further and create useShortestPath hook:

reusable stateful logic:

const useShortestPath = (spAlgorithm) => {
  const shortestPath = useMemo(() => new ShortestPath(spAlgorithm), []);
  const [version, setVersion] = useState(shortestPath.getVersion());

  useEffect(() => {
     shortestPath.updateAlgorithm(spAlgorithm);
  }, [spAlgorithm]);

  const onAddWayPoint = (x) => {
    shortestPath.addWayPoint(x);
    // Check if we need to rerender
    setVersion(shortestPath.getVersion());
  }

  return [onAddWayPoint, version]
}

presentational part:

const ShortestPathRenderer = ({spAlgorithm }) => {
  const [onAddWayPoint, version] = useShortestPath(spAlgorithm);

  return (
    ... // Render waypoints from shortestPath
  )
}

Upvotes: 1

f278f1b2
f278f1b2

Reputation: 311

You can switch class-based in example in functional analog just using useState hook

function ShortestPathRenderer({ spAlgorithm }) {
  const [shortestPath] = useRef(new ShortestPath(spAlgorithm)); // use ref to store ShortestPath instance
  const [version, setVersion] = useState(shortestPath.current.getVersion()); // state

  const onAddWayPoint = x => {
    shortestPath.current.addWayPoint(x);
    setVersion(shortestPath.current.getVersion());
  }

  useEffect(() => {
    shortestPath.current.updateAlgorithm(spAlgorithm);
  }, [spAlgorithm]);

  // ...
}

Upvotes: 1

Related Questions