I will first suggest some miscellaneous changes to the code and then answer your questions.
A better count
handling
I think that naming the state count
is a bit misleading because it is used as an offset to fetch the todos. This may count as a nit, but when you look at the code as a whole I think it is more accurate to use offset
instead of count
.
Usually, when I have a bit of state that always mutates in the same way, I use useReducer
. This way, the code looks a bit cleaner:
const [offset, doubleOffset] = useReducer((c) => c + 2, 1);
...
<button onClick={doubleOffset}>Fetch More</button>
In this particular case, we remove the need for a click handler. The updater function of useReducer
is enough, and the code is more concise.
Space and unnecessary braces
This can also be a bit subjective, but I find that adding spaces and blank lines properly improves readability a lot. I would update your code to be like this, only adding blank lines and removing extra braces:
const [count, setCount] = useState(1);
const [titles, setTitles] = useState([]);
useEffect(() => {
async function fetchData() {
const URL1 = `https://jsonplaceholder.typicode.com/todos/${count}`;
const URL2 = `https://jsonplaceholder.typicode.com/todos/${count + 1}`;
const apiData = await Promise.all([
axios.get(URL1),
axios.get(URL2)
]).then((res) => res.map((x) => x.data));
setTitles((prevTitles) => [...prevTitles, ...apiData]);
}
fetchData();
}, [count]);
const fetchMore = () => setCount(count + 2);
Services
Usually, an easier way to think about interactions of your app with another app is to treat them as black boxes, which means trying to call functions so your component's logic is decoupled from other modules in your app, like handling API calls, or generating a pdf, or opening a WebSocket. This is called service because those functions provide services for your UI logic.
This also means that it is easier to mock and test your code, so that is a plus. This allows for better code colocation, because usually, services group code that is related, so it is easier to have a wider view of the abstraction.
I know that I am using somewhat subjective terms here, but I hope you'll understand what I mean with the code examples.
Todos API
In this particular example, the service would be the interface with the Todos API. So I think that it's fair to extract that logic to a separate file.
Let's call the service todosApi
and the containing file todosApi.js
. Inside this file, we will define the functions that our service exposes and export an object for components to use.
Usually, different services that are related are placed in the same folder, for example, say we have a likes
API that we want to query besides our todos
API. Then we would have an index.js
re-exporting those services so that we have auto-import and autocomplete in our IDE.
// todosApi.js
import axios from 'axios';
const getTodoById = id => { ... };
export const todosApi = {
getTodoById
};
Using the service
Now that we have our service setup, we use it inside our component:
// App.js
useEffect(() => {
async function fetchData() {
const apiData = await todosApi.getTodoById(offset);
...
}
fetchData();
}, [ ... ]);
Notice that this removes the need to import axios
in our component.
A better structure
Personally, I think that code split into simple, well-defined functions is a bit better than having logic pervading the codebase, so I polished the code until I was comfortable with it. My fork is here.
// todosApi.js
import axios from "axios";
const getTodoById = (id) =>
axios
.get(`https://jsonplaceholder.typicode.com/todos/${id}`)
.then((res) => res.data);
const fetchTodoPair = (offset) =>
Promise.all([getTodoById(offset), getTodoById(offset + 1)]);
export const todosApi = {
getTodoById,
fetchTodoPair
};
// App.js
import { useEffect, useReducer, useState } from "react";
import { todosApi } from "./todosApi";
import "./styles.css";
export default function App() {
const [offset, doubleOffset] = useReducer((c) => c + 2, 1);
const [titles, setTitles] = useState([]);
useEffect(() => {
const fetchTodoPair = async () => {
const newTitles = await todosApi.fetchTodoPair(offset);
setTitles((prevTitles) => [...prevTitles, ...newTitles]);
};
fetchTodoPair();
}, [offset]);
return (
<div className="App">
<div className="title">Fetch Todos</div>
<ul>
{titles &&
titles.map((x) => (
<li key={x.id}>
{x.id} - {x.title}
</li>
))}
</ul>
<button onClick={doubleOffset}>Fetch More</button>
</div>
);
}