Skip to main content

React Ecosystem: State management with Redux

In the previous post, we created a blog post application with React and managed local state with useState hook. We will extend the same application and will introduce Redux and react-redux library for state management and @reduxjs/toolkit for opinionating setting up the redux store and creating selector function on state.

Adding required libraries

Run npm i --save @reduxjs/toolkit react-redux @types/react-redux from root of the project to add required set of libraries for this post.

Which new functions we will be using in this example?

  • configureStore from @reduxjs/toolkit
  • createSlice from @reduxjs/toolkit
  • useDispatch from react-redux
  • useSelctor from react-redux
  • useEffect from react

@reduxjs/toolkit: configureStore

This function provides a convenient abstraction over createStore function of redux library. It adds good defaults to the store for better experience(e.g. DevTools, Redux-Thunk for async actions).

@reduxjs/toolkit: createSlice

This function accepts an initial state, list of reducers and a 'slice name' and automatically generates action creators and action types for the reducer. You can also pass extraReducers to it for handling other complex reductions.

react-redux: useDispatch

This hook let's you access dispatch function of redux.

react-redux: useSelector

This Hook let's you tap on redux state and filter content. It takes selector function and optional equality function for state. If you require complex selector (memoized), then reselect library is a good choice. In this example, we will use simple selector on state.

React: useEffect

This hook is a combination of componentDidMount, componentDidUpdate and componentWillUnmount lifecycle methods of React. This hooks accepts the imperative function which can return the cleanup function as return statement; which will get executed on before every re-render. You can read a detailed post in React docs.

Configuring redux store

Create store.ts under src/redux folder.

import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
import blogPostsReducer from './reducer/blogPostsSlice';

export const store = configureStore({
  reducer: {
    blogPosts: blogPostsReducer,
  },
});

// Defining the root state
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
  ReturnType,
  RootState,
  unknown,
  Action<string>
>;

We are passing blogPostsReducer in the reducer parameter to configureStore function. We will be creating this reducer shorlty. Also, We have defined two types; one is RootState which defines the type of Root reducer and other is Appthunk which defines the ThunkAction (Async function).

Creating reducer for blog posts

Create blogPostsSlice.ts under src/redux/reducer folder.

Let's first add interface BlogPostsState which defines the state this slice will hold.

interface BlogPostsState {
  posts: IBlogPost[]
}

// Initial state of the reducer
const initialState: BlogPostsState = {
  posts: []
}

We will use createSlice function from @reduxjs/toolkit.

const blogPostsSlice = createSlice({
  name: 'blogPosts',
  initialState,
  reducers: {
   // We will soon pass the reducers here
  }
});

In the previous post, we managed all the local state in the BlogPosts.tsx component. We will start by moving `posts` stored in the local state to redux state. Define the setPosts reducer function under reducers property of the slice that we are creating.

const blogPostsSlice = createSlice({
  name: 'blogPosts',
  initialState,
  reducers: {
    setPosts: (state, action: PayloadAction<IBlogPost[]>) => {
      /*1.*/state.posts = action.payload
      // Alternate solution
      // return { ...state, posts: action.payload }
    }
  }
});

//actions froms slice
/*Line 2*/const { setPosts } = blogPostsSlice.actions;

// Async action functions
/*Line 3*/const setPostsAsync = (posts: IBlogPost[]): AppThunk => dispatch => {
  setTimeout(() => {
    dispatch(setPosts(posts))
  }, 500)
}

// Selector functions
/*Line 4*/ const selectPosts = (state: RootState) => state.blogPosts.posts;

/*Line 5*/export { selectPosts };

//action functions
/*Line 6*/export { setPosts, setPostsAsync };

// reducer
/*Line 7*/ export default blogPostsSlice.reducer;

At line 1, we are mutating the redux state directly. Don't worry, the state passed in the function as first argument is not the actual redux state but proxy on it. It uses immer library under the hood to manage and update the state(recreate). Alternatively, you can return your state object but can't do both(Mutating state and return new state object).

At line 2, we are getting actions created by createSlice function.

At line 3, we are creating async function to update posts. We are mimicking the async nature by setTimeout method. But, in real world, it would be replaced by API call to backend.

At Line 4, we have created selector function for posts. We are exporting selector function, actions and reducer at line 5, 6 and 7 respectively.

Update BlogPosts.tsx component

import { useDispatch, useSelector } from 'react-redux';
import { selectPosts, setPostsAsync } from '../redux/reducer/blogPostsSlice';

Use useEffect hook to update redux state with posts.

function BlogPosts(props: IBlogPostsProps) {
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(setPostsAsync(props.posts))
  }, [ props.posts, dispatch ]);


  ...
}

Replace local state management for posts with selecting state from redux store

/*Remove this line*/ //const [ posts, setPosts ] = useState(props.posts)
/*Add this line*/ const posts = useSelector(selectPosts);

Update onSearch function and replace it setPosts method with dispatch method.

function onSearch() {
  if (searchText !== '') {
    const foundPosts = props.posts.filter(filterPost)
    setShowingPost(findFirstPost(foundPosts))
    dispatch(setPostsAsync(foundPosts))
  } else {
    setShowingPost(findFirstPost(props.posts))
    dispatch(setPostsAsync(props.posts))
  }
}

Update index.tsx

Update index.tsx and wrap component with Provider component.

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
);

Now, run the application with npm run start command. The application will load as before but only change is we are reffering posts from redux store.

Update BlogPosts.tsx and replace all the local state with redux management state.

interface IBlogPostsProps {
  posts: Array<IBlogPost>
}


function BlogPosts(props: IBlogPostsProps) {
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(setPostsAsync(props.posts))
    dispatch(setShowingPostsAsync(props.posts && props.posts.length > 0 ? props.posts[0].id : 0))
  }, [ props.posts, dispatch ]);

  function findFirstPost(posts: Array<IBlogPost>) : IBlogPost | null {
    return posts && posts.length > 0 ? posts[0] : null;
  }

  const posts = useSelector(selectPosts);
  const showingPost = useSelector(selectShowingPost);
  const searchText = useSelector(selectSearchText);
  const selectedSearchOn = useSelector(selectSelectedSearchOn);

  function onBlogPostLinkClick(id: number): void {
    dispatch(setShowingPostsAsync(id));
  }
  
  function onChangeHandler(value: string, searchType: SearchType) : void {
   if (SearchType.SEARCH_TEXT === searchType) {
     dispatch(setSearchText(value));
   } else {
     dispatch(setSelectedSearchOn(value === SearchOnFields.TAG ? SearchOnFields.TAG : SearchOnFields.TITLE))
   }
  }

  function isMatched(value: string) {
    return value.toLowerCase().includes(searchText.toLowerCase())
  }

  function filterPost(post: IBlogPost) {
    if (selectedSearchOn === 'title') {
      return isMatched(post.title)
    } else {
      return post.tags.some(isMatched)
    }
  }

  function onSearch() {
    if (searchText !== '') {
      const foundPosts = props.posts.filter(filterPost)
      dispatch(setShowingPostsAsync(findFirstPost(foundPosts)?.id ?? 0))
      dispatch(setPostsAsync(foundPosts))
    } else {
      dispatch(setPostsAsync(props.posts))
      dispatch(setShowingPostsAsync(findFirstPost(props.posts)?.id ?? 0))
    }
  }

  return (
    <div className="blog-container">
      <BlogListing
        showingPost={showingPost?.id ?? 0}
        blogPosts={posts.map(post => { return {id: post.id, title: post.title }})}
        onClick={onBlogPostLinkClick}
        searchText={searchText}
        onSearchChange={onChangeHandler}
        onSearchButtonClick={onSearch}
        selectedSearchOn={selectedSearchOn}
      />
      {!!showingPost ? <BlogPost post={showingPost}/>: null }
    </div>
  );
}

export default BlogPosts;

Refactoring components

Before introducing redux, we managed whole state in the top level component aka BlogPosts.tsx and were passing the various variables to the child components. After introducing redux for state management, we don't require to pass on the various variables to child components. They can query it directly from the redux store using selector functions. Let's update all the components.

Updating BlogPost.tsx

function BlogPost() {
  /*Line 1*/const post = useSelector(selectShowingPost);
  return !!post ? (
    <div className='blog-post'>
      <div className='blog-post-title'>{post.title}</div>
      <div className='blog-post-body'>{post.content}</div>
      <div className='blog-post-footer'>
        <div className='blog-author'>{`By ${post.author} at ${post.postedOn}`}</div>
        <div className='blog-tags'>
          <div key='tags-label'>Tags: </div>
          {post.tags.map(tag => <div key={tag}>{tag}</div>)}
        </div>
      </div>
    </div>
  ) : (<></>);
}

export default BlogPost;

Explanation: We have removed the props and is using useSelector hook to get the current selectedPost for showing.

Updating BlogSearch.tsx

function BlogSearch() {
  const dispatch = useDispatch();

  /*Line 1*/const searchText = useSelector(selectSearchText);
  /*Line 2*/const selectedSearchOn = useSelector(selectSelectedSearchOn);

  function onSearchTextChange(event: ChangeEvent<HTMLInputElement>): void {
    /*Line 3*/dispatch(setSearchText(event.target.value));
  }

  function onSearchOnChange(event: ChangeEvent<HTMLSelectElement>): void {
    /*Line 4*/dispatch(setSelectedSearchOn(event.target.value === SearchOnFields.TAG ? SearchOnFields.TAG: SearchOnFields.TITLE));
  }

  return(
    <div className="blog-search-container">
      <div className='blog-search-title'>Search Blog</div>
      <div className='blog-search-body'>
        <input type="text" className="form-control" autoComplete="off" value={searchText ?? ''} onChange={onSearchTextChange}/>
        <select value={selectedSearchOn} className='form-control' onChange={onSearchOnChange}>
          <option value={SearchOnFields.TAG}>Tags</option>
          <option value={SearchOnFields.TITLE}>Title</option>
        </select>
        <button type="button" className="form-button" onClick={() => { /*Line 5*/dispatch(onSearchAsync()) }}>Search</button>
      </div>
    </div>
  );
}

export default BlogSearch;

Explanation: We have removed the props proeprty and type. At Line 1 and 2, we are using selector functions and useSelector hook to get searchText and selectedSearchOn from redux state respectively. At Line 3 and 4, we are using setSearchText and setSelectedSearchOn actions(redux) to update searchText and selectedSearchOn state in redux store respectively. At Line 5, we are calling onSearch action(blogPostsSlice) and updates the state in redux store for searchResults.

Update BlogListing.tsx

function BlogListing() {
  /*Line 1*/const blogPosts: IBlogPostListing[] = useSelector(selectPostsForListing);
  /*Line 2*/const showingPostId = useSelector(selectShowingPostId);

  const dispatch = useDispatch();

  return(
    <div className='blog-listing'>
      <BlogSearch/>
      <ul className="blog-posts">
        {
          blogPosts.map(post => <li className={showingPostId === post.id ? 'active' : ''} key={post.id} onClick={() => /*Line 3*/dispatch(setShowingPostsAsync(post.id))}>{post.title}</li>)
        }
      </ul>
    </div>
  );
}

export default BlogListing;

We have removed the props from BlogListing and also deleted IBlogListing type. At Line 1 and 2, we are getting state directly using selector function for posts and currently showing Blog post id respectively. At Line 3, we are triggering setShowingPostsAsync action created in blogPostsSlice.ts.

Updating BlogPosts.tsx

function BlogPosts(props: IBlogPostsProps) {
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(setPostsAsync(props.posts))
    dispatch(setShowingPostsAsync(props.posts && props.posts.length > 0 ? props.posts[0].id : 0))
  }, [ props.posts, dispatch ]);

  return (
    <div className="blog-container">
      <BlogListing/>
      <BlogPost/>
    </div>
  );
}

export default BlogPosts;

We have removed all the local state and function which we were passing to the children components. Now, we only are using React useEffect hook to update the redux state with posts.

That's it. :). You can get the full source code of this example from github.

Recap

In this post, we first added new libraries (react-redux, @reduxjs/toolkit). We explained few specific functions that we will be using in this example. Then, we created store.ts, blogPostsSlice.ts and started with replacing local state of posts from BlogPosts.tsx. Then, we replaced searchText, selectedSearchOn and showingPost from local state to redux state. We also added few selector functions. At last, we refactored our example and removed most of the method and variable reference from the top level component BlogPosts.tsx and added those in the respective components.

What's next?

In the next post, we will introduce server side rendering with Next.js. So, stay tuned!

Comments

Popular posts from this blog

Data Analytics: Watching and Alerting on real-time changing data in Elasticsearch using Kibana and SentiNL

In the previous post , we have setup ELK stack and ran data analytics on application events and logs. In this post, we will discuss how you can watch real-time application events that are being persisted in the Elasticsearch index and raise alerts if condition for watcher is breached using SentiNL (Kibana plugin). Few examples of alerting for application events ( see previous posts ) are: Same user logged in from different IP addresses. Different users logged in from same IP address. PermissionFailures in last 15 minutes. Particular kind of exception in last 15 minutes/ hour/ day. Watching and alerting on Elasticsearch index in Kibana There are many plugins available for watching and alerting on Elasticsearch index in Kibana e.g. X-Pack , SentiNL . X-Pack is a paid extension provided by elastic.co which provides security, alerting, monitoring, reporting and graph capabilities. SentiNL is free extension provided by siren.io which provides alerting and reporting function

React Ecosystem: Building BlogPost application with React and React Hooks

Building a Blog Post application Let's create a blog post application. It will have below features: Option to search blog posts. Option to list blog posts. Option to show blog post. Create a new project with npx create-react-app react-blog-posts --template typescript . Create BlogPosts.tsx component under src/components folder and IBlogPost model under src/models . import React from 'react'; import IBlogPost from '../models/IBlogPost'; interface IBlogPostsProps { posts: Array<IBlogPost> } function BlogPosts(props: IBlogPostsProps) { return ( <div className="blog-container"> <ul className="blog-posts"> { props.posts.map(post => <li key={post.id}>{post.title}</li>) } </ul> </div> ); } export default BlogPosts; interface IBlogPost { id: number title: string content: string author: string postedOn: string tags: string[]

Java 8 - Lambda expressions

In this post, we will cover following topics. What are Lambda expressions? Syntax for Lambda expression. How to define no parameter Lambda expression? How to define single/ multi parameter Lambda expression? How to return value from Lambda expression? Accessing local variables in Lambda expression. Target typing in Lambda expression. What are Lambda expressions? Lambda expressions are the first step of Java towards functional programming. Lambda expressions enable us to treat functionality as method arguments, express instances of single-method classes more compactly. Syntax for Lambda expression Lambda has three parts: comma separated list of formal parameters enclosed in parenthesis. arrow token -> . and, body of expression (which may or may not return value). (param) -> { System.out.println(param); } Lambda expression can only be used where the type they are matched are functional interfaces . How to define no parameter Lambda expression? If the la