Query messages and 'bold' part of a message. Show one query at a time but get the next query in advance for better user experience In this react component I'm trying to display one message on the screen at a time. This message has a 'bold' substring in it which should be accounted for. There's 2 endpoints here,

message to retrieve a message, and

bold which retrieves what is bold (with start and end keys to a substring) about the message passed in as a parameter


I tried to optimize for user experience by always having two messages available, so that when the user clicks 'next', there already is a message to display and no waiting time. These are currentMessage and nextMessage.
I used two useState hooks as I found looping inside of a useEffect hook while fetching to be somewhat complicated. Would still love to be able to do that.
Moreover, I have used two useEffects. The first is simply run once, to get the initial message, and the other is run when the page renders and then every time the user clicks 'next'. When a user clicks next, that's when the app swaps the current message for the one which was in the 'queue'
const MessageBox = () =&gt; {
  const [message, setCurrentMessage] = useState(&quot;&quot;);
  const [bold, setCurrentBold] = useState(&quot;&quot;);
  const [nextMessage, setNextMessage] = useState(&quot;&quot;);
  const [nextBold, setNextBold] = useState(&quot;&quot;);
  const [isGettingMessage, setIsGettingMessage] = useState(true);

  useEffect(() =&gt; {
    fetch(`https://myapi.com/message`)
      .then((res) =&gt; res.json())
      .then((res) =&gt; {
        if (res.data) {
          setCurrentMessage(res.data.message);
          getBold(res.data.message, setCurrentBold);
        } else {
          console.log(&quot;handle err&quot;);
        }
      });
  }, []);

  useEffect(() =&gt; {
    if (!isGettingMessage) return;
    fetch(`https://myapi.com/message`)
      .then((res) =&gt; res.json())
      .then((res) =&gt; {
        if (res.data) {
          setNextMessage(res.data.message);
          getBold(res.data.message, setNextBold);
        } else {
          console.log(&quot;handle err&quot;);
        }
      });
    setIsGettingMessage(false);
  }, [isGettingMessage]);

  const getBold = (message, boldSetter) =&gt; {
    if (!message) return;
    fetch(`https://myapi.com/bold`, {
      method: &quot;POST&quot;,
      headers: { &quot;Content-type&quot;: &quot;application/json&quot; },
      body: JSON.stringify({ content: message }),
    })
      .then((res) =&gt; res.json())
      .then((res) =&gt; {
        if (res?.data?.bold.length) {
          boldSetter(res.data.bold);
          return;
        } else if (res.error) {
          console.log(&quot;handle err&quot;);
        }
        boldSetter(&quot;&quot;);
      });
  };

  const formatMessageAndBold = () =&gt; {
    if (message &amp;&amp; bold) {
      return boldMessage;
    } else if (message &amp;&amp; !bold) {
      return &lt;span&gt;{message}&lt;/span&gt;;
    }
  };

  const boldMessage = useMemo(
    () =&gt; (
      &lt;p&gt;
        &lt;span&gt;{message.substring(0, bold.start)}&lt;/span&gt;
        &lt;strong&gt;{message.substring(bold.start, bold.end)}&lt;/strong&gt;
        &lt;span&gt;{message.substring(bold.end)}&lt;/span&gt;
      &lt;/p&gt;
    ),
    [message, bold.start, bold.end]
  );

  const messageAndBold = formatMessageAndBold();

  const onClick = () =&gt; {
    if (isGettingMessage) return;
    setIsGettingMessage(true);
    setCurrentMessage(nextMessage);
    setCurrentBold(nextBold);
  };

  return (
    &lt;div&gt;
      {messageAndBold}
      &lt;button isLoading={nextMessage === message || !message} onClick={onClick}&gt;
        {message ? &quot;Next Message&quot; : &quot;Loading...&quot;}
      &lt;/button&gt;
    &lt;/div&gt;
  );
};

export default MessageBox;
```
note: this took me longer then I expected and there may be some syntax errors, but contains the general idea
Once your component starts having complex logic, it's a good indication to start breaking it up.
Here I propose putting your logic in some different files:

constants.js
helpers.js
hooks.js
MessageBox.js

Notably, in hooks.js I would create a few custom hooks to handle your logic.

useQueryFullMessage - handles fetching a message + a bold and returning the result
useFullMessage - uses useQueryFullMessage twice to get two full messages and handles the logic on preloading the next full message.

This is a possible abstraction (among many) you can use.
I also don't believe that useMemo is required in this situation... unless you're going to be loading many messages. React by default is already pretty optmized.
//constants.js
const API_MESSAGE_URL = 'https://myapi.com/message';
const API_BOLD_URL = 'https://myapi.com/bold';

//helpers.js
import { API_MESSAGE_URL, API_BOLD_URL } from './constants';

const getMessage = async () =&gt; {
  const res = await fetch(API_MESSAGE_URL);
  const {data} = await res.json() ?? {};
  return data?.message;
}

const getBold = async (message) =&gt; {

  const res = await fetch(API_BOLD_URL, {
    method: &quot;POST&quot;,
    headers: { &quot;Content-type&quot;: &quot;application/json&quot; },
    body: JSON.stringify({ content: message }),
  });
  const {data} = await res.json() ?? {};
  return data?.bold;
}

export const getFullMessage = async () =&gt; {
  const message = await getMessage();
  if(!message) {
    // handle error
    return {};
  }
  const bold = await getBold(message);
  if(!bold) {
    // handle error
  }
  return { 
    message,
    bold
  };
}

//hooks.js
import { getFullMessage } from './helpers';

const useQueryFullMessage = () =&gt; {
  const [fullMessaage, setFullMessage] = useState();
  const [loading, setLoading] = useState(true);
  const [preloadNextValue, togglePreloadNext] = useState(0);
  useEffect(async () =&gt; {
    if(!loading) {
      setLoading(true);
    }
    const fullMessage = await getFullMessage();
    // handle if bold not set
    setFullMessage(fullMessage);
    setLoading(false);
  }, [preloadNextValue]);

  return {
    fullMessage,
    loading,
    preloadNext: () =&gt; togglePreloadNext(preloadNextValue + 1),
    setFullMessage,
  }
}

export const useFullMessage = () =&gt; {
  const {
    fullMessage: currentFullMessage,
    loading: loadingCurrentFullMessage,
    setFullMessage: setCurrentFullMessage
  } = useQueryFullMessage();
  const {
    fullMessage: nextFullMessage,
    loading: loadingNextFullMessage,
    preloadNext
  } = useQueryFullMessage();

  const requestNextMessage = () =&gt; {
    if(loadingCurrentFullMessage || loadingNextFullMessage) {
      return;
    }
    setCurrentFullMessage(nextFullMessage);
    preloadNext();
  }

  return {
    currentFullMessage,
    loadingCurrentFullMessage,
    nextFullMessage,
    loadingNextFullMessage,
    requestNextMessage
  };
}

// MessageBox.js
import { useFullMesage } from './hooks';
const MessageBox = () =&gt; {
  const {
    currentFullMessage,
    loadingCurrentFullMessage,
    nextFullMessage,
    loadingNextFullMessage,
    requestNextMessage
  } = useFullMessage();

  const { message, bold } = fullMessage;
  // TODO: check if message and bold are set.

  const formatMessageAndBold = () =&gt; {
    if (message &amp;&amp; bold) {
      return &lt;p&gt;
        &lt;span&gt;{message.substring(0, bold.start)}&lt;/span&gt;
        &lt;strong&gt;{message.substring(bold.start, bold.end)}&lt;/strong&gt;
        &lt;span&gt;{message.substring(bold.end)}&lt;/span&gt;
      &lt;/p&gt;;
    } else if (message &amp;&amp; !bold) {
      return &lt;span&gt;{message}&lt;/span&gt;;
    }
  };

  return (
    &lt;div&gt;
      {loadingCurrentFullMessage ? &quot;Loading...&quot; : formatMessageAndBold()}
      &lt;button isLoading={loadingNextFullMessage} onClick={requestNextMessage}&gt;
        {!loadingNextFullMessage ? &quot;Next Message&quot; : &quot;Loading...&quot;}
      &lt;/button&gt;
    &lt;/div&gt;
  );
}
```