Here is a step-by-step of what's happening:
function App() {
const { data } = useFetch({ url : "jack.json" })
console.log('App rendering')
return (
<div className="App">
<div>Hello</div>
<div>{JSON.stringify(data)}</div> )
}
Before I begin, it may help to also understand how the react component lifecycle works:

From the start, it's just like calling a regular function...
If you refer to the diagram above, we are at the stage called render, because the body of a functional component is the equivalent of the render function in class components.
First, useFetch is called
const { data } = useFetch({ url : "jack.json" });
Inside useFetch
const [data, setData] = useState(null);
At this point, data is null
Next useEffect is called
useEffect(() => {
console.log("useFetch useEffect");
fetch(options.url)
.then( response => response.json())
.then( json => setData(json))
}, []);
useEffect creates an effect watcher. This is basically a function which is invoked every time one of its dependencies changes. However, it is not invoked until after the first time the component renders.
If you refer to the diagram above, the watcher will only be invoked for the first time at the point called componentDidMount.
Back inside App:
(image reposted to avoid scrolling)
console.log('App rendering')
We get a nice console.log output...
return (
<div className="App">
<div>Hello</div>
<div>{JSON.stringify(data)}</div>
);
Here we return control back to react, and it will now attempt to take our html and mount it inside the dom.
Note that data is still null. Every other component react encounters will go through the same process described above.
So far, the steps leading up to this point can be summarized as Mounting (See the diagram above), and we have now reached the componentDidMount stage.
componentDidMount
React will now invoke our effect.
Note: If you had more than one effect, react will invoke all of them, and none will be skipped for this first round of effectual work that react does. Also they will all be invoked in the same order you declared them in.
useEffect watcher invoked
console.log("useFetch useEffect");
Another nice printout to console.log
fetch(options.url)
.then( response => response.json())
.then( json => setData(json))
fetch is invoked, but since we are not allowed to wait for promises inside effects, the fetch just runs.
Note: If your effect returns any function, react saves it to run it the next time an update/unmount occurs.
At this point, we just kinda idle until something interesting hap...oh wait fetch is done.
State Updates (setData(...))
(image reposted to avoid scrolling)
.then( json => setData(json))
setData will update the state of the component. If you refer to the diagram above once again, you'll see that when a state update occurs, the next step is to render.
Therefore, react will once again repeat the same steps as above, but it does not re-create the useEffect, or the useState again, because it has stored them from the previous render.
Sidenote: This is also how react is able to detect when you are calling a hook (useEffect, useState, etc) conditionally. If react detects a new hook after the first render is finished, it will warn you of this
App is rendered again
console.log('App rendering')
Next...
return (
<div className="App">
<div>Hello</div>
<div>{JSON.stringify(data)}</div>
);
And we once again return the html content of our component (this time data is now whatever was sent to setData).
Updating
As you can tell, we are now in the Updating Phase. From now on, react will simply wait for props/state updates, and runs any effects that depend on them in the componentDidUpdate stage of the lifecycle
Sidenote: Your useEffect should depend on options.url so that it is re-run when the url changes
useEffect(() => {
console.log("useFetch useEffect");
fetch(options.url)
.then( response => response.json())
.then( json => setData(json))
}, [options?.url]);
Unmounting
If by some bad luck, your component is unmounted either by the parent, or by some strange magic, your component will be moved into the Unmounting phase. Once again, refer to the diagram above
In this phase, none of the effects are run. Instead, any functions returned by your "effect watcher", will be invoked to do cleanup.
Note: All the cleanup functions will be run in the order they were encountered (typically top-down)