1
\$\begingroup\$

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 = () => {
 const [message, setCurrentMessage] = useState("");
 const [bold, setCurrentBold] = useState("");
 const [nextMessage, setNextMessage] = useState("");
 const [nextBold, setNextBold] = useState("");
 const [isGettingMessage, setIsGettingMessage] = useState(true);
 useEffect(() => {
 fetch(`https://myapi.com/message`)
 .then((res) => res.json())
 .then((res) => {
 if (res.data) {
 setCurrentMessage(res.data.message);
 getBold(res.data.message, setCurrentBold);
 } else {
 console.log("handle err");
 }
 });
 }, []);
 useEffect(() => {
 if (!isGettingMessage) return;
 fetch(`https://myapi.com/message`)
 .then((res) => res.json())
 .then((res) => {
 if (res.data) {
 setNextMessage(res.data.message);
 getBold(res.data.message, setNextBold);
 } else {
 console.log("handle err");
 }
 });
 setIsGettingMessage(false);
 }, [isGettingMessage]);
 const getBold = (message, boldSetter) => {
 if (!message) return;
 fetch(`https://myapi.com/bold`, {
 method: "POST",
 headers: { "Content-type": "application/json" },
 body: JSON.stringify({ content: message }),
 })
 .then((res) => res.json())
 .then((res) => {
 if (res?.data?.bold.length) {
 boldSetter(res.data.bold);
 return;
 } else if (res.error) {
 console.log("handle err");
 }
 boldSetter("");
 });
 };
 const formatMessageAndBold = () => {
 if (message && bold) {
 return boldMessage;
 } else if (message && !bold) {
 return <span>{message}</span>;
 }
 };
 const boldMessage = useMemo(
 () => (
 <p>
 <span>{message.substring(0, bold.start)}</span>
 <strong>{message.substring(bold.start, bold.end)}</strong>
 <span>{message.substring(bold.end)}</span>
 </p>
 ),
 [message, bold.start, bold.end]
 );
 const messageAndBold = formatMessageAndBold();
 const onClick = () => {
 if (isGettingMessage) return;
 setIsGettingMessage(true);
 setCurrentMessage(nextMessage);
 setCurrentBold(nextBold);
 };
 return (
 <div>
 {messageAndBold}
 <button isLoading={nextMessage === message || !message} onClick={onClick}>
 {message ? "Next Message" : "Loading..."}
 </button>
 </div>
 );
};
export default MessageBox;
```
asked Apr 28, 2021 at 12:53
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

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 () => {
 const res = await fetch(API_MESSAGE_URL);
 const {data} = await res.json() ?? {};
 return data?.message;
}
const getBold = async (message) => {
 const res = await fetch(API_BOLD_URL, {
 method: "POST",
 headers: { "Content-type": "application/json" },
 body: JSON.stringify({ content: message }),
 });
 const {data} = await res.json() ?? {};
 return data?.bold;
}
export const getFullMessage = async () => {
 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 = () => {
 const [fullMessaage, setFullMessage] = useState();
 const [loading, setLoading] = useState(true);
 const [preloadNextValue, togglePreloadNext] = useState(0);
 useEffect(async () => {
 if(!loading) {
 setLoading(true);
 }
 const fullMessage = await getFullMessage();
 // handle if bold not set
 setFullMessage(fullMessage);
 setLoading(false);
 }, [preloadNextValue]);
 return {
 fullMessage,
 loading,
 preloadNext: () => togglePreloadNext(preloadNextValue + 1),
 setFullMessage,
 }
}
export const useFullMessage = () => {
 const {
 fullMessage: currentFullMessage,
 loading: loadingCurrentFullMessage,
 setFullMessage: setCurrentFullMessage
 } = useQueryFullMessage();
 const {
 fullMessage: nextFullMessage,
 loading: loadingNextFullMessage,
 preloadNext
 } = useQueryFullMessage();
 const requestNextMessage = () => {
 if(loadingCurrentFullMessage || loadingNextFullMessage) {
 return;
 }
 setCurrentFullMessage(nextFullMessage);
 preloadNext();
 }
 return {
 currentFullMessage,
 loadingCurrentFullMessage,
 nextFullMessage,
 loadingNextFullMessage,
 requestNextMessage
 };
}
// MessageBox.js
import { useFullMesage } from './hooks';
const MessageBox = () => {
 const {
 currentFullMessage,
 loadingCurrentFullMessage,
 nextFullMessage,
 loadingNextFullMessage,
 requestNextMessage
 } = useFullMessage();
 const { message, bold } = fullMessage;
 // TODO: check if message and bold are set.
 const formatMessageAndBold = () => {
 if (message && bold) {
 return <p>
 <span>{message.substring(0, bold.start)}</span>
 <strong>{message.substring(bold.start, bold.end)}</strong>
 <span>{message.substring(bold.end)}</span>
 </p>;
 } else if (message && !bold) {
 return <span>{message}</span>;
 }
 };
 return (
 <div>
 {loadingCurrentFullMessage ? "Loading..." : formatMessageAndBold()}
 <button isLoading={loadingNextFullMessage} onClick={requestNextMessage}>
 {!loadingNextFullMessage ? "Next Message" : "Loading..."}
 </button>
 </div>
 );
}
```
answered Apr 28, 2021 at 22:56
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.