Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using redux, how to avoid rendering old state data before making a new API call?

I've just started to do some experiments with redux and react-redux.

Note: I'm using Hooks, so I'm using useSelector() and useDispatch() custom hooks from react-redux.

Imagine I have some piece of state to get a blogPost from DB based on a slug.

My state.blogPost:

{
  loading: false,      // TRUE while fetching
  error: null,         // TO keep network errors
  blogPost: null       // TO keep the blogPost
}

BlogPostContainer.js

I'm dispatching the actions and the async call inside a useEffect.

Note1: I'm doing the async job manually, cause I'm not using redux-thunk yet.

Note2: I'm using react-router-dom so the slugs come from props.match.params.slug.

Example: myApp.net/blog/some-blog-post-slug


// ...

const { loading, error, blogPost } = useSelector(state => state.blogPost);
const dispatch = useDispatch();

useEffect(() => {

  dispatch({ type: "GET_BLOGPOST_START", payload: props.match.params.slug });  // START ACTION

  mockAPI(props.match.params.slug)

  .then((result) => {
    dispatch({ type: "GET_BLOGPOST_SUCCESS", payload: result });               // SUCCESS ACTION
  })

  .catch((err) => {
    dispatch({ type: "GET_BLOGPOST_FAIL", payload: err });                     // FAIL ACTION
  })

}, [dispatch,props.match.params.slug]);

// RETURN STATEMENT

return (
  loading ?
    <div>Loading...</div>
  : error ?
    <ErrorPage/>
  : <BlogPostPage blogPost={blogPost}/>
);

QUESTION

This is working fine for the first loaded blogPost (because the state is completely empty at that point).

Now imagine that I go back to the home page an click on another blogPost.

From the second time on, when my BlogPostContainer renders, there's already something present in my state (either a blogPost or an error that was loaded last time for the previous blogPost slug).

And as a result of this, I see a flicker of that information displayed on screen right before my useEffect runs. When it runs, loading will be set to true by the GET_BLOGPOST_START action, and I'll see the loading.

I would like to see the loading page immediately. Without any flickering with old state data.

How do people usually handle this while using redux?

  • Do I need a local loading state?

  • Should I dispatch an action to turn loading to true when my component unmounts? So it will be already true when my component mounts again for the next slug?

  • Should I use useLayoutEffect so the effect will run before rendering and I won't see the flickering? (it works, but it feels wrong).

  • What is the best solution for this kind of situation?


EXTRA

My reducer:

const initialState = {
  loading: false,
  error: null,
  blogPost: null
};

function getBlogPostReducer(state = initialState, action) {
  switch (action.type) {
    case "GET_BLOGPOST_START":        // SET LOADING TO TRUE
      return {
        ...initialState,
        loading: true
      };
    case "GET_BLOGPOST_SUCCESS":      // SET BLOGPOST and LOADING TO FALSE
      return {
        ...initialState,
        loading: false,
        blogPost: action.payload
      };
    case "GET_BLOGPOST_FAIL":         // SET ERROR and LOADING TO FALSE
      return {
        ...initialState,
        loading: false,
        error: action.payload
      };
    default:
      return state;
  }
}

export default getBlogPostReducer;
like image 903
cbdeveloper Avatar asked Mar 02 '23 23:03

cbdeveloper


1 Answers

In the useEffect return a cleanup function that will dispatch a "clear blog post" action when the component unmounts:

useEffect(() => {
  dispatch({ type: "GET_BLOGPOST_START", payload: props.match.params.slug });  // START ACTION

  mockAPI(props.match.params.slug)

  .then((result) => {
    dispatch({ type: "GET_BLOGPOST_SUCCESS", payload: result });               // SUCCESS ACTION
  })

  .catch((err) => {
    dispatch({ type: "GET_BLOGPOST_FAIL", payload: err });                     // FAIL ACTION
  })
  
  return () => dispatch({ type: "CLEAR_BLOGPOST" });

}, [dispatch,props.match.params.slug]);

And the reducer handler should remove both error and blogpost entries from the state:

case "CLEAR_BLOGPOST":
  return initialState;
like image 110
Ori Drori Avatar answered Mar 05 '23 16:03

Ori Drori