Home > other >  React Custom Hook Behind the scene
React Custom Hook Behind the scene

Time:02-19

the result of the code is

  1. first call
  2. outside useEffect
  3. before return
  4. inside useEffect
  5. first call
  6. outside useEffect
  7. before return
  8. inside useEffect
  9. first call
  10. outside useEffect
  11. before return

but why?

  • how inside useEffect be called twice
  • how after outside useEffect there is no inside useEffect
  • why it did not make infinite loop because data is being re-created every time so it should effect useApi
function useApi(data, initial){
    const [name, setName] = useState(initial)
    console.log("outside useEffect")
    useEffect(() => {
        console.log("inside useEffect")
        fetch(data.name,{
           method: data.method,
           body: data.body
       }).then((response) => response.json()).then((response) => {
           setName(response.id)
       })
    }, [data])
    return [name, setName]
}

function App() {
  console.log("first call")
  const data =  {
    name: "http://localhost:8000/api/kebap",
    method: "GET"
  }
  const [info, setInfo] = useApi(data,
   "10")
  console.log("before return")
  return <div> {info} </div>
}

CodePudding user response:

  const data = {
    name: 'http://localhost:8000/api/kebap',
    method: 'GET',
  };

This object, const data is being created every render. It is also used as the a dependency for your useEffect hook. This means that the useEffect hook will be called every time you render, causing an infinite loop.

The solution, is to memoize the object, or in this case, simply move it outside of the component.

function useApi(data, initial) {
  const [name, setName] = useState(initial);
  console.log('outside useEffect');
  useEffect(() => {
    console.log('inside useEffect');
    fetch(data.name, {
      method: data.method,
      body: data.body,
    })
      .then((response) => response.json())
      .then((response) => {
        setName(response.id);
      });
  }, [data]);
  return [name, setName];
}

const data = {
  name: 'http://localhost:8000/api/kebap',
  method: 'GET',
};

function App() {
  console.log('first call');
  const [info, setInfo] = useApi(data, '10');
  console.log('before return');
  return <div> {info} </div>;
}

Another way, would be to use useMemo. This would be useful, if your data object would be dynamically created.

function App() {
  console.log('first call');
  const data = useMemo(() => ({
    name: 'http://localhost:8000/api/kebap',
    method: 'GET',
  }),[]);
  const [info, setInfo] = useApi(data, '10');
  console.log('before return');
  return <div> {info} </div>;
}

CodePudding user response:

Using a better custom hook, usePromise, your App would look like this. Remember to always handle the fetching and error states of your app.

function App() {
  const [{ fetching, error, data = "10"}, retry] = usePromise(_ =>
    fetch("http://localhost:8000/api/kebap")
      .then(res => res.json())
  )
  return <div>
    { fetching
    ? <p>Fetching...</p>
    : error
    ? <p>Error: {error.message}</p>
    : <pre>{JSON.stringify(data, null, 2)}</pre>
    }
  </div>
}

Here is a minimal verifiable example of usePromise.

  • Input of () => Promise
  • Returns [state, retry]
    • state contains {fetching, error, data} so you can know the status of the promise
    • retry is a function that allows you to rerun the promise
function usePromise(thunk) {
  const [state, setState] = React.useState({
    fetching: true,
    error: null,
    data: null
  })
  const [timestamp, setTimestamp] = React.useState(Date.now())
  React.useEffect(_ => {
    setState({ ...state, fetching: true })
    thunk()
      .then(data => setState({ fetching: false, error: null, data }))
      .catch(error => setState({ fetching: false, error, data: null }))
  }, [timestamp])
  return [state, _ => setTimestamp(Date.now())]
}

Here is a demo. Click the retry button several times to see the effect run each time.

function App() {
  const [{fetching, error, data}, retry] = usePromise(_ =>
    fetch("https://random-data-api.com/api/app/random_app").then(res => res.json())
  )
  return <div>
    <button type="button" onClick={retry} children="retry" disabled={fetching} />
    <pre>
      { fetching
      ? "Fetching..."
      : error
      ? `Error: ${error.message}`
      : JSON.stringify(data, null, 2)
      }
    </pre>
  </div>
}

function usePromise(thunk) {
  const [state, setState] = React.useState({
    fetching: true,
    error: null,
    data: null
  })
  const [timestamp, setTimestamp] = React.useState(Date.now())
  React.useEffect(_ => {
    setState({ ...state, fetching: true })
    thunk()
      .then(data => setState({ fetching: false, error: null, data }))
      .catch(error => setState({ fetching: false, error, data: null }))
  }, [timestamp])
  return [state, _ => setTimestamp(Date.now())]
}

ReactDOM.render(<App/>, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>

Most likely retry will not be implemented on a button like retry. Instead you can check for error and call retry() automatically in your codes.

  • Related