Matias Seguel
Matias Seguel

Reputation: 828

how to implement a refresh button with React Hooks?

I'm trying to implement a refresh button but can't get it done.

This is how my code looks like:

// ParentComponent.js
const ParentComponent = () => {
  const { loading, error, data } = useItems();

  return (
    <ChildComponent items={data} />
  );

  ... rest of my code that shows the data
};

// ChildComponent.js
const ChildComponent = ({ items }) => { 
  return (
    // Logic that renders the items in <li>s
    <button onClick={() => console.log('Clicking this button should refresh parent component')}
  )
};

// services/useItems.js
const useItems = () => {
  const [items, setItems] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');

  useEffect(() => {
    axios
      .get(API_URL + '/counter')
      .then((response) => {
        setItems(response.data);
        setLoading(false);
      })
      .catch((error) => {
        setLoading(false);
        setError(error.message);
      });
    }, []);

    return { loading, error, data: counters };

}

I've tried several ways but none did the work. any helps would be truly appreciated :)

Upvotes: 2

Views: 8301

Answers (3)

Rajesh
Rajesh

Reputation: 24915

There are couple fo small parts where you need to make changes to resolve issue.

  1. You need to create a communication for refresh
    • Create a function to process any processing for refresh.
    • Pass this as a prop to child component
    • In child component, call it on necessary event, in this case click
  2. Now since you are using hooks, you need to get it invoked.
    • You can add a function refreshData in your useItem hook and expose it
    • Call this function on click of button.
    • You will also have to add a flag in hooks and update useEffect to be triggered on its change
    • This function is necessary as setItems is only available inside hook.

Following is a working sample:

const { useState, useEffect } = React;
// ParentComponent.js
const ParentComponent = () => {
  const { loading, error, data, refreshData } = useItems();
  const refreshFn = () => {
    refreshData()
  }

  return (
    <ChildComponent
      items={data}
      onClick={refreshFn}/>
  );

  // ... rest of my code that shows the data
};

// ChildComponent.js
const ChildComponent = ({ items, onClick }) => { 
  const onClickFn = () => {
  console.log('Clicking this button should refresh parent component')
    if(!!onClick) {
      onClick();
    }
  }
  return (
    // Logic that renders the items in <li>s
    <div>
      <button
        onClick={ () => onClickFn() }
      >Refresh</button>
      <ul>
        {
          items.map((item) => <li key={item}>{item}</li>)
        }
      </ul>
    </div>
  )
};

// services/useItems.js
const useItems = () => {
  const [items, setItems] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');
  const [refresh, setRefresh] = useState(false)

  useEffect(() => {
    if (refresh) {
      setItems(Array.from({ length: 5 }, () => Math.random()));
      setRefresh(false)
    }
  }, [ refresh ]);

  return {
    loading,
    error,
    data: items,
    refreshData: () => setRefresh(true)
  };
}

ReactDOM.render(<ParentComponent/>, document.querySelector('.content'))
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div class='content'></div>


As correctly commented by hackape, we need to add a check for refresh and fetch data only if its true

Upvotes: 1

Christian Fritz
Christian Fritz

Reputation: 21364

I don't think useEffect is the right mechanism here. Since it's an imperative call, nothing reactive about it, useState does the job just fine:

// ParentComponent.js
const ParentComponent = () => {
  const [items, setItems] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');

  const refresh = () => {
    axios.get(API_URL + '/counter').then((response) => {
      setItems(response.data);
      setLoading(false);
    }).catch((error) => {
      setLoading(false);
      setError(error.message);
    });
  };
  useEffect(refresh, []);

  return (
    <ChildComponent items={items} refresh={refresh} />
  );

  // ... rest of my code that shows the data
};

// ChildComponent.js
const ChildComponent = ({ items, refresh }) => { 
  return (
    // Logic that renders the items in <li>s
    <button onClick={refresh}>
      Refresh
    </button>
  )
};

Upvotes: 6

hackape
hackape

Reputation: 19957

A very simple trick is to increase an integer state, let's just call it version, which would trigger a re-render of <ParentComponent /> and if useEffect depends on version, it'll re-execute the callback, so you get the "refresh" effect.

// ParentComponent.js
const ParentComponent = () => {
  const [version, setVersion] = useState(0)
  // when called, add 1 to "version"
  const refresh = useCallback(() => {
    setVersion(s => s + 1)
  }, [])

  const { loading, error, data } = useItems(version);

  return (
    <ChildComponent items={data} refresh={refresh} />
  );
};

// ChildComponent.js
const ChildComponent = ({ items, refresh }) => { 
  return (
    // Logic that renders the items in <li>s
    <button onClick={refresh} />
  )
};

// services/useItems.js
const useItems = (version) => {
  const [items, setItems] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');

  useEffect(() => {
    axios
      .get(API_URL + '/counter')
      .then((response) => {
        setItems(response.data);
        setLoading(false);
      })
      .catch((error) => {
        setLoading(false);
        setError(error.message);
      });
    }, [version]);  // <-- depend on "version"

    return { loading, error, data: counters };

}

Upvotes: 1

Related Questions