DEV Community

Anthony Humphreys
Anthony Humphreys

Posted on • Originally published at reacthooks.dev

useSyncedState

Synced State Experiment

After working on useLocalStorage, I wondered how hard it would be to sync state to persistent, distributed storage. For my 5th Day of 100 Days of code I decided to make a first attempt at this idea.

I followed the same pattern as for building the useLocalStorage hook, extending the useState API, and triggering a useEffect on the state update to handle the state synchronization...asynchronously8.

Without further ado, here's the code...I'll be working more on this. At the moment, this might be useful for a use case such as building a user profile. A common poor experience is filling out some information and boom you've hit refresh, or swiped back on the trackpad...this scenario is already resolved by the localStorage hook, but I thought it would be cool to explore binding state to an API. The current implementation is geared around a REST API, so next steps will be to look at passing a query/mutation to the hook.

I'm also thinking about how to hook this up to a useReducer, passing in the reducer function to determine how to manage state.

import { useState, useCallback, useReducer, useEffect } from "react";

type State = {
  success: boolean;
  loading: boolean;
  error: boolean;
};

type Action = {
  type: "loading" | "success" | "error";
  syncedState?: object;
};

const initialState: State = {
  success: false,
  loading: false,
  error: false,
};

const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case "success":
      return { success: true, loading: false, error: false };
    case "loading":
      return { success: false, loading: true, error: false };
    case "error":
      return { success: false, loading: false, error: true };
    default:
      return state;
  }
};

const SYNC_URL = "https://localhost:3000";

export const useSyncedState = (
  key: string,
  initialValue: string,
  delay: number = 1000,
  syncUrl: string
): [State, any, Function] => {
  const [state, dispatch] = useReducer(reducer, initialState);

  const syncToServer = useCallback(async (valueToStore: object) => {
    dispatch({ type: "loading" });
    const response = await fetch(SYNC_URL, {
      method: "POST",
      headers: new Headers({ "Content-Type": "application/json" }),
      body: JSON.stringify(valueToStore),
    });
    response.ok ? dispatch({ type: "success" }) : dispatch({ type: "error" });
  }, []);

  const syncToClient = useCallback(async () => {
    dispatch({ type: "loading" });
    const response = await fetch(SYNC_URL, {
      method: "GET",
      headers: new Headers({ "Content-Type": "application/json" }),
    });
    response.ok
      ? dispatch({ type: "success", syncedState: await response.json() })
      : dispatch({ type: "error" });
    return response.json();
  }, []);

  const [syncedValue, setSyncedValue] = useState<object>(async () => {
    try {
      const syncedState = await syncToClient();
      return syncedState ?? initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });

  const setValue = (value: any) => {
    try {
      const valueToStore =
        value instanceof Function ? value(syncedValue) : value;
      setSyncedValue(valueToStore);
    } catch (error) {
      console.error(error);
    }
  };

  useEffect(() => {
    const timeout = setTimeout(() => {
      syncToServer(syncedValue);
    }, delay);
    return () => clearTimeout(timeout);
  }, [syncedValue, delay, syncToServer]);

  return [state, syncedValue, setValue];
};
Enter fullscreen mode Exit fullscreen mode

Would be curious to hear anyone's thoughts on this, I can imagine lots of questions about the motivation and to be perfectly honest...it just seemed like a cool thing to put together 🤷‍♂️

Top comments (0)