Browsing Tag: Redux

    web design

    Building A Web App With React, Redux And Sanity.io — Smashing Magazine

    02/11/2021

    About The Author

    Ifeanyi Dike is a full-stack developer in Abuja, Nigeria. He’s the team lead at Sterling Digitals Limited but also open to more opportunities and …
    More about
    Ifeanyi

    Headless CMS is a powerful and easy way to manage content and access API. Built on React, Sanity.io is a seamless tool for flexible content management. It can be used to build simple to complex applications from the ground up.

    In this article, we’ll build a simple listing app with Sanity.io and React. Our global states will be managed with Redux and the application will be styled with styled-components.

    The fast evolution of digital platforms have placed serious limitations on traditional CMS like WordPress. These platforms are coupled, inflexible and are focused on the project, rather than the product. Thankfully, several headless CMS have been developed to tackle these challenges and many more.

    Unlike traditional CMS, headless CMS, which can be described as Software as a Service (SaaS), can be used to develop websites, mobile apps, digital displays, and many more. They can be used on limitless platforms. If you are looking for a CMS that is platform independent, developer-first, and offers cross platform support, you need not look farther from headless CMS.

    A headless CMS is simply a CMS without a head. The head here refers to the frontend or the presentation layer while the body refers to the backend or the content repository. This offers a lot of interesting benefits. For instance, it allows the developer to choose any frontend of his choice and you can also design the presentation layer as you want.

    There are lots of headless CMS out there, some of the most popular ones include Strapi, Contentful, Contentstack, Sanity, Butter CMS, Prismic, Storyblok, Directus, etc. These headless CMS are API-based and have their individual strong points. For instance, CMS like Sanity, Strapi, Contentful, and Storyblok are free for small projects.

    These headless CMS are based on different tech stacks as well. While Sanity.io is based on React.js, Storyblok is based on Vue.js. As a React developer, this is the major reason why I quickly picked interest in Sanity. However, being a headless CMS, each of these platforms can be plugged on any frontend, whether Angular, Vue or React.

    Each of these headless CMS has both free and paid plans which represent significant price jump. Although these paid plans offer more features, you wouldn’t want to pay all that much for a small to mid-sized project. Sanity tries to solve this problem by introducing pay-as-you-go options. With these options, you will be able to pay for what you use and avoid the price jump.

    Another reason why I choose Sanity.io is their GROQ language. For me, Sanity stands out from the crowd by offering this tool. Graphical-Relational Object Queries (GROQ) reduces development time, helps you get the content you need in the form you need it, and also helps the developer to create a document with a new content model without code changes.

    Moreover, developers are not constrained to the GROQ language. You can also use GraphQL or even the traditional axios and fetch in your React app to query the backend. Like most other headless CMS, Sanity has comprehensive documentation that contains helpful tips to build on the platform.

    Note: This article requires a basic understanding of React, Redux and CSS.

    Getting Started With Sanity.io

    To use Sanity in your machine, you’ll need to install the Sanity CLI tool. While this can be installed locally on your project, it is preferable to install it globally to make it accessible to any future applications.

    To do this, enter the following commands in your terminal.

    npm install -g @sanity/cli

    The -g flag in the above command enables global installation.

    Next, we need to initialize Sanity in our application. Although this can be installed as a separate project, it is usually preferable to install it within your frontend app (in this case React).

    In her blog, Kapehe explained in detail how to integrate Sanity with React. It will be helpful to go through the article before continuing with this tutorial.

    Enter the following commands to initialize Sanity in your React app.

    sanity init

    The sanity command becomes available to us when we installed the Sanity CLI tool. You can view a list of the available Sanity commands by typing sanity or sanity help in your terminal.

    When setting up or initializing your project, you’ll need to follow the prompts to customize it. You’ll also be required to create a dataset and you can even choose their custom dataset populated with data. For this listing app, we will be using Sanity’s custom sci-fi movies dataset. This will save us from entering the data ourselves.

    To view and edit your dataset, cd to the Sanity subdirectory in your terminal and enter sanity start. This usually runs on http://localhost:3333/. You may be required to login to access the interface (make sure you login with the same account you used when initializing the project). A screenshot of the environment is shown below.

    Sanity server overview
    An overview of the sanity server for the sci-fi movie dataset. (Large preview)

    Sanity-React Two-way Communication

    Sanity and React need to communicate with each other for a fully functional application.

    CORS Origins Setting In Sanity Manager

    We’ll first connect our React app to Sanity. To do this, login to https://manage.sanity.io/ and locate CORS origins under API Settings in the Settings tab. Here, you’ll need to hook your frontend origin to the Sanity backend. Our React app runs on http://localhost:3000/ by default, so we need to add that to the CORS.

    This is shown in the figure below.

    CORS origin settings
    Setting CORS origin in Sanity.io Manager. (Large preview)

    Connecting Sanity To React

    Sanity associates a project ID to every project you create. This ID is needed when connecting it to your frontend application. You can find the project ID in your Sanity Manager.

    The backend communicates with React using a library known as sanity client. You need to install this library in your Sanity project by entering the following commands.

    npm install @sanity/client

    Create a file sanitySetup.js (the filename does not matter), in your project src folder and enter the following React codes to set up a connection between Sanity and React.

    import sanityClient from "@sanity/client"
    export default sanityClient({
        projectId: PROJECT_ID,
        dataset: DATASET_NAME,
        useCdn: true
    });

    We passed our projectId, dataset name and a boolean useCdn to the instance of the sanity client imported from @sanity/client. This works the magic and connects our app to the backend.

    Now that we’ve completed the two-way connection, let’s jump right in to build our project.

    Setting Up And Connecting Redux To Our App

    We’ll need a few dependencies to work with Redux in our React app. Open up your terminal in your React environment and enter the following bash commands.

    npm install redux react-redux redux-thunk
    

    Redux is a global state management library that can be used with most frontend frameworks and libraries such as React. However, we need an intermediary tool react-redux to enable communication between our Redux store and our React application. Redux thunk will help us to return a function instead of an action object from Redux.

    While we could write the entire Redux workflow in one file, it is often neater and better to separate our concerns. For this, we will divide our workflow into three files namely, actions, reducers, and then the store. However, we also need a separate file to store the action types, also known as constants.

    Setting Up The Store

    The store is the most important file in Redux. It organizes and packages the states and ships them to our React application.

    Here is the initial setup of our Redux store needed to connect our Redux workflow.

    import { createStore, applyMiddleware } from "redux";
    import thunk from "redux-thunk";
    import reducers from "./reducers/";
    
    export default createStore(
      reducers,
      applyMiddleware(thunk)
    );
    

    The createStore function in this file takes three parameters: the reducer (required), the initial state and the enhancer (usually a middleware, in this case, thunk supplied through applyMiddleware). Our reducers will be stored in a reducers folder and we’ll combine and export them in an index.js file in the reducers folder. This is the file we imported in the code above. We’ll revisit this file later.

    Introduction To Sanity’s GROQ Language

    Sanity takes querying on JSON data a step further by introducing GROQ. GROQ stands for Graph-Relational Object Queries. According to Sanity.io, GROQ is a declarative query language designed to query collections of largely schema-less JSON documents.

    Sanity even provides the GROQ Playground to help developers become familiar with the language. However, to access the playground, you need to install sanity vision.
    Run sanity install @sanity/vision on your terminal to install it.

    GROQ has a similar syntax to GraphQL but it is more condensed and easier to read. Furthermore, unlike GraphQL, GROQ can be used to query JSON data.

    For instance, to retrieve every item in our movie document, we’ll use the following GROQ syntax.

    *[_type == "movie"]

    However, if we wish to retrieve only the _ids and crewMembers in our movie document. We need to specify those fields as follows.

    `*[_type == 'movie']{                                             
        _id,
        crewMembers
    }
    

    Here, we used * to tell GROQ that we want every document of _type movie. _type is an attribute under the movie collection. We can also return the type like we did the _id and crewMembers as follows:

    *[_type == 'movie']{                                             
        _id,
        _type,
        crewMembers
    }
    

    We’ll work more on GROQ by implementing it in our Redux actions but you can check Sanity.io’s documentation for GROQ to learn more about it. The GROQ query cheat sheet provides a lot of examples to help you master the query language.

    Setting Up Constants

    We need constants to track the action types at every stage of the Redux workflow. Constants help to determine the type of action dispatched at each point in time. For instance, we can track when the API is loading, fully loaded and when an error occurs.

    We don’t necessarily need to define constants in a separate file but for simplicity and clarity, this is usually the best practice in Redux.

    By convention, constants in Javascript are defined with uppercase. We’ll follow the best practices here to define our constants. Here is an example of a constant for denoting requests for moving movie fetching.

    export const MOVIE_FETCH_REQUEST = "MOVIE_FETCH_REQUEST";

    Here, we created a constant MOVIE_FETCH_REQUEST that denotes an action type of MOVIE_FETCH_REQUEST. This helps us to easily call this action type without using strings and avoid bugs. We also exported the constant to be available anywhere in our project.

    Similarly, we can create other constants for fetching action types denoting when the request succeeds or fails. A complete code for the movieConstants.js is given in the code below.

    Here we have defined several constants for fetching a movie or list of movies, sorting and fetching the most popular movies. Notice that we set constants to determine when the request is loading, successful and failed.

    Similarly, our personConstants.js file is given below:

    export const PERSONS_FETCH_REQUEST = "PERSONS_FETCH_REQUEST";
    export const PERSONS_FETCH_SUCCESS = "PERSONS_FETCH_SUCCESS";
    export const PERSONS_FETCH_FAIL = "PERSONS_FETCH_FAIL";
    
    export const PERSON_FETCH_REQUEST = "PERSON_FETCH_REQUEST";
    export const PERSON_FETCH_SUCCESS = "PERSON_FETCH_SUCCESS";
    export const PERSON_FETCH_FAIL = "PERSON_FETCH_FAIL";
    
    export const PERSONS_COUNT = "PERSONS_COUNT";

    Like the movieConstants.js, we set a list of constants for fetching a person or persons. We also set a constant for counting persons. The constants follow the convention described for movieConstants.js and we also exported them to be accessible to other parts of our application.

    Finally, we’ll implement light and dark mode in the app and so we have another constants file globalConstants.js. Let’s take a look at it.

    export const SET_LIGHT_THEME = "SET_LIGHT_THEME";
    export const SET_DARK_THEME = "SET_DARK_THEME";

    Here we set constants to determine when light or dark mode is dispatched. SET_LIGHT_THEME determines when the user switches to the light theme and SET_DARK_THEME determines when the dark theme is selected. We also exported our constants as shown.

    Setting Up The Actions

    By convention, our actions are stored in a separate folder. Actions are grouped according to their types. For instance, our movie actions are stored in movieActions.js while our person actions are stored in personActions.js file.

    We also have globalActions.js to take care of toggling the theme from light to dark mode.

    Let’s fetch all movies in moviesActions.js.

    import sanityAPI from "../../sanitySetup";
    import {
      MOVIES_FETCH_FAIL,
      MOVIES_FETCH_REQUEST,
      MOVIES_FETCH_SUCCESS  
    } from "../constants/movieConstants";
    
    const fetchAllMovies = () => async (dispatch) => {
      try {
        dispatch({
          type: MOVIES_FETCH_REQUEST
        });
        const data = await sanityAPI.fetch(
          `*[_type == 'movie']{                                            
              _id,
              "poster": poster.asset->url,
          } `
        );
        dispatch({
          type: MOVIES_FETCH_SUCCESS,
          payload: data
        });
      } catch (error) {
        dispatch({
          type: MOVIES_FETCH_FAIL,
          payload: error.message
        });
      }
    };

    Remember when we created the sanitySetup.js file to connect React to our Sanity backend? Here, we imported the setup to enable us to query our sanity backend using GROQ. We also imported a few constants exported from the movieConstants.js file in the constants folder.

    Next, we created the fetchAllMovies action function for fetching every movie in our collection. Most traditional React applications use axios or fetch to fetch data from the backend. But while we could use any of these here, we’re using Sanity’s GROQ. To enter the GROQ mode, we need to call sanityAPI.fetch() function as shown in the code above. Here, sanityAPI is the React-Sanity connection we set up earlier. This returns a Promise and so it has to be called asynchronously. We’ve used the async-await syntax here, but we can also use the .then syntax.

    Since we are using thunk in our application, we can return a function instead of an action object. However, we chose to pass the return statement in one line.

    const fetchAllMovies = () => async (dispatch) => {
      ...
    }

    Note that we can also write the function this way:

    const fetchAllMovies = () => {
      return async (dispatch)=>{
        ...
      }
    }

    In general, to fetch all movies, we first dispatched an action type that tracks when the request is still loading. We then used Sanity’s GROQ syntax to asynchronously query the movie document. We retrieved the _id and the poster url of the movie data. We then returned a payload containing the data gotten from the API.

    Similarly, we can retrieve movies by their _id, sort movies, and get the most popular movies.

    We can also fetch movies that match a particular person’s reference. We did this in the fetchMoviesByRef function.

    const fetchMoviesByRef = (ref) => async (dispatch) => {
      try {
        dispatch({
          type: MOVIES_REF_FETCH_REQUEST
        });
        const data = await sanityAPI.fetch(
          `*[_type == 'movie' 
                && (castMembers[person._ref match '${ref}'] || 
                    crewMembers[person._ref match '${ref}'])            
                ]{                                             
                    _id,                              
                    "poster" : poster.asset->url,
                    title
                } `
        );
        dispatch({
          type: MOVIES_REF_FETCH_SUCCESS,
          payload: data
        });
      } catch (error) {
        dispatch({
          type: MOVIES_REF_FETCH_FAIL,
          payload: error.message
        });
      }
    };

    This function takes an argument and checks if person._ref in either the castMembers or crewMembers matches the passed argument. We return the movie _id, poster url, and title alongside. We also dispatch an action of type MOVIES_REF_FETCH_SUCCESS, attaching a payload of the returned data, and if an error occurs, we dispatch an action of type MOVIE_REF_FETCH_FAIL, attaching a payload of the error message, thanks to the try-catch wrapper.

    In the fetchMovieById function, we used GROQ to retrieve a movie that matches a particular id passed to the function.

    The GROQ syntax for the function is shown below.

    const data = await sanityAPI.fetch(
          `*[_type == 'movie' && _id == '${id}']{                                               
                    _id,
                    "cast" :
                        castMembers[]{
                            "ref": person._ref,
                            characterName, 
                            "name": person->name,
                            "image": person->image.asset->url
                        }
                    ,
                    "crew" :
                        crewMembers[]{
                            "ref": person._ref,
                            department, 
                            job,
                            "name": person->name,
                            "image": person->image.asset->url
                        }
                    ,                
                    "overview":   {                    
                        "text": overview[0].children[0].text
                      },
                    popularity,
                    "poster" : poster.asset->url,
                    releaseDate,                                
                    title
                }[0]`
        );

    Like the fetchAllMovies action, we started by selecting all documents of type movie but we went further to select only those with an id supplied to the function. Since we intend to display a lot of details for the movie, we specified a bunch of attributes to retrieve.

    We retrieved the movie id and also a few attributes in the castMembers array namely ref, characterName, the person’s name, and the person’s image. We also changed the alias from castMembers to cast.

    Like the castMembers, we selected a few attributes from the crewMembers array, namely ref, department, job, the person’s name and the person’s image. we also changed the alias from crewMembers to crew.

    In the same way, we selected the overview text, popularity, movie’s poster url, movie’s release date and title.

    Sanity’s GROQ language also allows us to sort a document. To sort an item, we pass order next to a pipe operator.

    For instance, if we wish to sort movies by their releaseDate in ascending order, we could do the following.

    const data = await sanityAPI.fetch(
          `*[_type == 'movie']{                                            
              ...
          } | order(releaseDate, asc)`
        );
    

    We used this notion in the sortMoviesBy function to sort either by ascending or descending order.

    Let’s take a look at this function below.

    const sortMoviesBy = (item, type) => async (dispatch) => {
      try {
        dispatch({
          type: MOVIES_SORT_REQUEST
        });
        const data = await sanityAPI.fetch(
          `*[_type == 'movie']{                                
                    _id,                                               
                    "poster" : poster.asset->url,    
                    title
                    } | order( ${item} ${type})`
        );
        dispatch({
          type: MOVIES_SORT_SUCCESS,
          payload: data
        });
      } catch (error) {
        dispatch({
          type: MOVIES_SORT_FAIL,
          payload: error.message
        });
      }
    };

    We began by dispatching an action of type MOVIES_SORT_REQUEST to determine when the request is loading. We then used the GROQ syntax to sort and fetch data from the movie collection. The item to sort by is supplied in the variable item and the mode of sorting (ascending or descending) is supplied in the variable type. Consequently, we returned the id, poster url, and title. Once the data is returned, we dispatched an action of type MOVIES_SORT_SUCCESS and if it fails, we dispatch an action of type MOVIES_SORT_FAIL.

    A similar GROQ concept applies to the getMostPopular function. The GROQ syntax is shown below.

    const data = await sanityAPI.fetch(
          `
                *[_type == 'movie']{ 
                    _id,                              
                    "overview":   {                    
                        "text": overview[0].children[0].text
                    },                
                    "poster" : poster.asset->url,    
                    title 
                }| order(popularity desc) [0..2]`
        );

    The only difference here is that we sorted the movies by popularity in descending order and then selected only the first three. The items are returned in a zero-based index and so the first three items are items 0, 1 and 2. If we wish to retrieve the first ten items, we could pass [0..9] to the function.

    Here’s the complete code for the movie actions in the movieActions.js file.

    Setting Up The Reducers

    Reducers are one of the most important concepts in Redux. They take the previous state and determine the state changes.

    Typically, we’ll be using the switch statement to execute a condition for each action type. For instance, we can return loading when the action type denotes loading, and then the payload when it denotes success or error. It is expected to take in the initial state and the action as arguments.

    Our movieReducers.js file contains various reducers to match the actions defined in the movieActions.js file. However, each of the reducers has a similar syntax and structure. The only differences are the constants they call and the values they return.

    Let’s start by taking a look at the fetchAllMoviesReducer in the movieReducers.js file.

    import {
      MOVIES_FETCH_FAIL,
      MOVIES_FETCH_REQUEST,
      MOVIES_FETCH_SUCCESS,  
    } from "../constants/movieConstants";
    
    const fetchAllMoviesReducer = (state = {}, action) => {
      switch (action.type) {
        case MOVIES_FETCH_REQUEST:
          return {
            loading: true
          };
        case MOVIES_FETCH_SUCCESS:
          return {
            loading: false,
            movies: action.payload
          };
        case MOVIES_FETCH_FAIL:
          return {
            loading: false,
            error: action.payload
          };
        case MOVIES_FETCH_RESET:
          return {};
        default:
          return state;
      }
    };

    Like all reducers, the fetchAllMoviesReducer takes the initial state object (state) and the action object as arguments. We used the switch statement to check the action types at each point in time. If it corresponds to MOVIES_FETCH_REQUEST, we return loading as true to enable us to show a loading indicator to the user.

    If it corresponds to MOVIES_FETCH_SUCCESS, we turn off the loading indicator and then return the action payload in a variable movies. But if it is MOVIES_FETCH_FAIL, we also turn off the loading and then return the error. We also want the option to reset our movies. This will enable us to clear the states when we need to do so.

    We have the same structure for other reducers. The complete movieReducers.js is shown below.

    We also followed the exact same structure for personReducers.js. For instance, the fetchAllPersonsReducer function defines the states for fetching all persons in the database.

    This is given in the code below.

    import {
      PERSONS_FETCH_FAIL,
      PERSONS_FETCH_REQUEST,
      PERSONS_FETCH_SUCCESS,
    } from "../constants/personConstants";
    
    const fetchAllPersonsReducer = (state = {}, action) => {
      switch (action.type) {
        case PERSONS_FETCH_REQUEST:
          return {
            loading: true
          };
        case PERSONS_FETCH_SUCCESS:
          return {
            loading: false,
            persons: action.payload
          };
        case PERSONS_FETCH_FAIL:
          return {
            loading: false,
            error: action.payload
          };
        default:
          return state;
      }
    };
    

    Just like the fetchAllMoviesReducer, we defined fetchAllPersonsReducer with state and action as arguments. These are standard setup for Redux reducers. We then used the switch statement to check the action types and if it’s of type PERSONS_FETCH_REQUEST, we return loading as true. If it’s PERSONS_FETCH_SUCCESS, we switch off loading and return the payload, and if it’s PERSONS_FETCH_FAIL, we return the error.

    Combining Reducer

    Redux’s combineReducers function allows us to combine more than one reducer and pass it to the store. We’ll combine our movies and persons reducers in an index.js file within the reducers folder.

    Let’s take a look at it.

    import { combineReducers } from "redux";
    import {
      fetchAllMoviesReducer,
      fetchMovieByIdReducer,
      sortMoviesByReducer,
      getMostPopularReducer,
      fetchMoviesByRefReducer
    } from "./movieReducers";
    
    import {
      fetchAllPersonsReducer,
      fetchPersonByIdReducer,
      countPersonsReducer
    } from "./personReducers";
    
    import { toggleTheme } from "./globalReducers";
    
    export default combineReducers({
      fetchAllMoviesReducer,
      fetchMovieByIdReducer,
      fetchAllPersonsReducer,
      fetchPersonByIdReducer,
      sortMoviesByReducer,
      getMostPopularReducer,
      countPersonsReducer,
      fetchMoviesByRefReducer,
      toggleTheme
    });

    Here we imported all the reducers from the movies, persons, and global reducers file and passed them to combineReducers function. The combineReducers function takes an object which allows us to pass all our reducers. We can even add an alias to the arguments in the process.

    We’ll work on the globalReducers later.

    We can now pass the reducers in the Redux store.js file. This is shown below.

    import { createStore, applyMiddleware } from "redux";
    import thunk from "redux-thunk";
    import reducers from "./reducers/index";
    
    export default createStore(reducers, initialState, applyMiddleware(thunk));
    

    Having set up our Redux workflow, let’s set up our react application.

    Setting Up Our React Application

    Our react application will list movies and their corresponding cast and crewmembers. We will be using react-router-dom for routing and styled-components for styling the app. We’ll also use Material UI for icons and some UI components.

    Enter the following bash command to install the dependencies.

    npm install react-router-dom @material-ui/core @material-ui/icons query-string

    Here’s what we’ll be building.

    Connecting Redux To Our React App

    React-redux ships with a Provider function that allows us to connect our application to the Redux store. To do this, we have to pass an instance of the store to the Provider. We can do this either in our index.js or App.js file.

    Here’s our index.js file.

    import React from "react";
    import ReactDOM from "react-dom";
    import "./index.css";
    import App from "./App";
    import { Provider } from "react-redux";
    import store from "./redux/store";
    ReactDOM.render(
      <Provider store={store}>
        <App />
      </Provider>,
      document.getElementById("root")
    );

    Here, we imported Provider from react-redux and store from our Redux store. Then we wrapped our entire components tree with the Provider, passing the store to it.

    Next, we need react-router-dom for routing in our React application. react-router-dom comes with BrowserRouter, Switch and Route that can be used to define our path and routes.

    We do this in our App.js file. This is shown below.

    import React from "react";
    import Header from "./components/Header";
    import Footer from "./components/Footer";
    import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
    import MoviesList from "./pages/MoviesListPage";
    import PersonsList from "./pages/PersonsListPage";
    
    function App() {
    
      return (
          <Router>
            <main className="contentwrap">
              <Header />
              <Switch>
                <Route path="/persons/">
                  <PersonsList />
                </Route>
                <Route path="/" exact>
                  <MoviesList />
                </Route>
              </Switch>
            </main>
            <Footer />
          </Router>
      );
    }
    export default App;

    This is a standard setup for routing with react-router-dom. You can check it out in their documentation. We imported our components Header, Footer, PersonsList and MovieList. We then set up the react-router-dom by wrapping everything in Router and Switch.

    Since we want our pages to share the same header and footer, we had to pass the <Header /> and <Footer /> component before wrapping the structure with Switch. We also did a similar thing with the main element since we want it to wrap the entire application.

    We passed each component to the route using Route from react-router-dom.

    Defining Our Pages And Components

    Our application is organized in a structured way. Reusable components are stored in the components folder while Pages are stored in the pages folder.

    Our pages comprise movieListPage.js, moviePage.js, PersonListPage.js and PersonPage.js. The MovieListPage.js lists all the movies in our Sanity.io backend as well as the most popular movies.

    To list all the movies, we simply dispatch the fetchAllMovies action defined in our movieAction.js file. Since we need to fetch the list as soon as the page loads, we have to define it in the useEffect. This is shown below.

    import React, { useEffect } from "react";
    import { fetchAllMovies } from "../redux/actions/movieActions";
    import { useDispatch, useSelector } from "react-redux";
    
    const MoviesListPage = () => {
      const dispatch = useDispatch();
      useEffect(() => {    
          dispatch(fetchAllMovies());
      }, [dispatch]);
    
      const { loading, error, movies } = useSelector(
        (state) => state.fetchAllMoviesReducer
      );
      
      return (
        ...
      )
    };
    export default MoviesListPage;
    

    Thanks to the useDispatch and useSelector Hooks, we can dispatch Redux actions and select the appropriate states from the Redux store. Notice that the states loading, error and movies were defined in our Reducer functions and here selected them using the useSelector Hook from React Redux. These states namely loading, error and movies become available immediately we dispatched the fetchAllMovies() actions.

    Once we get the list of movies, we can display it in our application using the map function or however we wish.

    Here is the complete code for the moviesListPage.js file.

    We started by dispatching the getMostPopular movies action (this action selects the movies with the highest popularity) in the useEffect Hook. This allows us to retrieve the most popular movies as soon as the page loads. Additionally, we allowed users to sort movies by their releaseDate and popularity. This is handled by the sortMoviesBy action dispatched in the code above. Furthermore, we dispatched the fetchAllMovies depending on the query parameters.

    Also, we used the useSelector Hook to select the corresponding reducers for each of these actions. We selected the states for loading, error and movies for each of the reducers.

    After getting the movies from the reducers, we can now display them to the user. Here, we have used the ES6 map function to do this. We first displayed a loader whenever each of the movie states is loading and if there’s an error, we display the error message. Finally, if we get a movie, we display the movie image to the user using the map function. We wrapped the entire component in a MovieListContainer component.

    The <MovieListContainer> … </MovieListContainer> tag is a div defined using styled components. We’ll take a brief look at that soon.

    Styling Our App With Styled Components

    Styled components allow us to style our pages and components on an individual basis. It also offers some interesting features such as inheritance, Theming, passing of props, etc.

    Although we always want to style our pages on an individual basis, sometimes global styling may be desirable. Interestingly, styled-components provide a way to do that, thanks to the createGlobalStyle function.

    To use styled-components in our application, we need to install it. Open your terminal in your react project and enter the following bash command.

    npm install styled-components

    Having installed styled-components, Let’s get started with our global styles.

    Let’s create a separate folder in our src directory named styles. This will store all our styles. Let’s also create a globalStyles.js file within the styles folder. To create global style in styled-components, we need to import createGlobalStyle.

    import { createGlobalStyle } from "styled-components";

    We can then define our styles as follows:

    export const GlobalStyle = createGlobalStyle`
      ...
    `

    Styled components make use of the template literal to define props. Within this literal, we can write our traditional CSS codes.

    We also imported deviceWidth defined in a file named definition.js. The deviceWidth holds the definition of breakpoints for setting our media queries.

    import { deviceWidth } from "./definition";

    We set overflow to hidden to control the flow of our application.

    html, body{
            overflow-x: hidden;
    }

    We also defined the header style using the .header style selector.

    .header{
      z-index: 5;
      background-color: ${(props)=>props.theme.midDarkBlue}; 
      display:flex;
      align-items:center;
      padding: 0 20px;
      height:50px;
      justify-content:space-between;
      position:fixed;
      top:0;
      width:100%;
      @media ${deviceWidth.laptop_lg}
      {
        width:97%;
      }
      ...
    }

    Here, various styles such as the background color, z-index, padding, and lots of other traditional CSS properties are defined.

    We’ve used the styled-components props to set the background color. This allows us to set dynamic variables that can be passed from our component. Moreover, we also passed the theme’s variable to enable us to make the most of our theme toggling.

    Theming is possible here because we have wrapped our entire application with the ThemeProvider from styled-components. We’ll talk about this in a moment. Furthermore, we used the CSS flexbox to properly style our header and set the position to fixed to make sure it remains fixed with respect to the browser. We also defined the breakpoints to make the headers mobile friendly.

    Here is the complete code for our globalStyles.js file.

    import { createGlobalStyle } from "styled-components";
    import { deviceWidth } from "./definition";
    
    export const GlobalStyle = createGlobalStyle`
        html{
            overflow-x: hidden;
        }
        body{
            background-color: ${(props) => props.theme.lighter};        
            overflow-x: hidden;   
            min-height: 100vh;     
            display: grid;
            grid-template-rows: auto 1fr auto;
        }
        #root{        
            display: grid;
            flex-direction: column;   
        }    
        h1,h2,h3, label{
            font-family: 'Aclonica', sans-serif;        
        }
        h1, h2, h3, p, span:not(.MuiIconButton-label), 
        div:not(.PrivateRadioButtonIcon-root-8), div:not(.tryingthis){
            color: ${(props) => props.theme.bodyText}
        }
        
        p, span, div, input{
            font-family: 'Jost', sans-serif;       
        }
        
        .paginate button{
            color: ${(props) => props.theme.bodyText}
        }
        
        .header{
            z-index: 5;    
            background-color: ${(props) => props.theme.midDarkBlue};                
            display: flex;
            align-items: center;   
            padding: 0 20px;        
            height: 50px;
            justify-content: space-between;
            position: fixed;
            top: 0;
            width: 100%;
            @media ${deviceWidth.laptop_lg}{
                width: 97%;            
            }               
            
            @media ${deviceWidth.tablet}{
                width: 100%;
                justify-content: space-around;
            }
            a{
                text-decoration: none;
            }
            label{
                cursor: pointer;
                color: ${(props) => props.theme.goldish};
                font-size: 1.5rem;
            }        
            .hamburger{
                cursor: pointer;   
                color: ${(props) => props.theme.white};
                @media ${deviceWidth.desktop}{
                    display: none;
                }
                @media ${deviceWidth.tablet}{
                    display: block;                
                }
            }  
                     
        }    
        .mobileHeader{
            z-index: 5;        
            background-color: ${(props) =>
              props.theme.darkBlue};                    
            color: ${(props) => props.theme.white};
            display: grid;
            place-items: center;        
            
            width: 100%;      
            @media ${deviceWidth.tablet}{
                width: 100%;                   
            }                         
            
            height: calc(100% - 50px);                
            transition: all 0.5s ease-in-out; 
            position: fixed;        
            right: 0;
            top: 50px;
            .menuitems{
                display: flex;
                box-shadow: 0 0 5px ${(props) => props.theme.lightshadowtheme};           
                flex-direction: column;
                align-items: center;
                justify-content: space-around;                        
                height: 60%;            
                width: 40%;
                a{
                    display: flex;
                    flex-direction: column;
                    align-items:center;
                    cursor: pointer;
                    color: ${(props) => props.theme.white};
                    text-decoration: none;                
                    &:hover{
                        border-bottom: 2px solid ${(props) => props.theme.goldish};
                        .MuiSvgIcon-root{
                            color: ${(props) => props.theme.lightred}
                        }
                    }
                }
            }
        }
        
        footer{                
            min-height: 30px;        
            margin-top: auto;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;        
            font-size: 0.875rem;        
            background-color: ${(props) => props.theme.midDarkBlue};      
            color: ${(props) => props.theme.white};        
        }    
    `;
    

    Notice that we wrote pure CSS code within the literal but there are a few exceptions. Styled-components allows us to pass props. You can learn more about this in the documentation.

    Apart from defining global styles, we can define styles for individual pages.

    For instance, here is the style for the PersonListPage.js defined in PersonStyle.js in the styles folder.

    import styled from "styled-components";
    import { deviceWidth, colors } from "./definition";
    
    export const PersonsListContainer = styled.div`
      margin: 50px 80px;
      @media ${deviceWidth.tablet} {
        margin: 50px 10px;
      }
      a {
        text-decoration: none;
      }
      .top {
        display: flex;
        justify-content: flex-end;
        padding: 5px;
        .MuiSvgIcon-root {
          cursor: pointer;
          &:hover {
            color: ${colors.darkred};
          }
        }
      }
      .personslist {
        margin-top: 20px;
        display: grid;
        place-items: center;
        grid-template-columns: repeat(5, 1fr);
        @media ${deviceWidth.laptop} {
          grid-template-columns: repeat(4, 1fr);
        }
        @media ${deviceWidth.tablet} {
          grid-template-columns: repeat(3, 1fr);
        }
        @media ${deviceWidth.tablet_md} {
          grid-template-columns: repeat(2, 1fr);
        }
        @media ${deviceWidth.mobile_lg} {
          grid-template-columns: repeat(1, 1fr);
        }
        grid-gap: 30px;
        .person {
          width: 200px;
          position: relative;
          img {
            width: 100%;
          }
          .content {
            position: absolute;
            bottom: 0;
            left: 8px;
            border-right: 2px solid ${colors.goldish};
            border-left: 2px solid ${colors.goldish};
            border-radius: 10px;
            width: 80%;
            margin: 20px auto;
            padding: 8px 10px;
            background-color: ${colors.transparentWhite};
            color: ${colors.darkBlue};
            h2 {
              font-size: 1.2rem;
            }
          }
        }
      }
    `;
    

    We first imported styled from styled-components and deviceWidth from the definition file. We then defined PersonsListContainer as a div to hold our styles. Using media queries and the established breakpoints, we made the page mobile-friendly by setting various breakpoints.

    Here, we have used only the standard browser breakpoints for small, large and very large screens. We also made the most of the CSS flexbox and grid to properly style and display our content on the page.

    To use this style in our PersonListPage.js file, we simply imported it and added it to our page as follows.

    import React from "react";
    
    const PersonsListPage = () => {
      return (
        <PersonsListContainer>
          ...
        </PersonsListContainer>
      );
    };
    export default PersonsListPage;
    

    The wrapper will output a div because we defined it as a div in our styles.

    Adding Themes And Wrapping It Up

    It’s always a cool feature to add themes to our application. For this, we need the following:

    • Our custom themes defined in a separate file (in our case definition.js file).
    • The logic defined in our Redux actions and reducers.
    • Calling our theme in our application and passing it through the component tree.

    Let’s check this out.

    Here is our theme object in the definition.js file.

    export const theme = {
      light: {
        dark: "#0B0C10",
        darkBlue: "#253858",
        midDarkBlue: "#42526e",
        lightBlue: "#0065ff",
        normal: "#dcdcdd",
        lighter: "#F4F5F7",
        white: "#FFFFFF",
        darkred: "#E85A4F",
        lightred: "#E98074",
        goldish: "#FFC400",
        bodyText: "#0B0C10",
        lightshadowtheme: "rgba(0, 0, 0, 0.1)"
      },
      dark: {
        dark: "white",
        darkBlue: "#06090F",
        midDarkBlue: "#161B22",
        normal: "#dcdcdd",
        lighter: "#06090F",
        white: "white",
        darkred: "#E85A4F",
        lightred: "#E98074",
        goldish: "#FFC400",
        bodyText: "white",
        lightshadowtheme: "rgba(255, 255, 255, 0.9)"
      }
    };
    

    We have added various color properties for the light and dark themes. The colors are carefully chosen to enable visibility both in light and dark mode. You can define your themes as you want. This is not a hard and fast rule.

    Next, let’s add the functionality to Redux.

    We have created globalActions.js in our Redux actions folder and added the following codes.

    import { SET_DARK_THEME, SET_LIGHT_THEME } from "../constants/globalConstants";
    import { theme } from "../../styles/definition";
    
    export const switchToLightTheme = () => (dispatch) => {
      dispatch({
        type: SET_LIGHT_THEME,
        payload: theme.light
      });
      localStorage.setItem("theme", JSON.stringify(theme.light));
      localStorage.setItem("light", JSON.stringify(true));
    };
    
    export const switchToDarkTheme = () => (dispatch) => {
      dispatch({
        type: SET_DARK_THEME,
        payload: theme.dark
      });
      localStorage.setItem("theme", JSON.stringify(theme.dark));
      localStorage.setItem("light", JSON.stringify(false));
    };

    Here, we simply imported our defined themes. Dispatched the corresponding actions, passing the payload of the themes we needed. The payload results are stored in the local storage using the same keys for both light and dark themes. This enables us to persist the states in the browser.

    We also need to define our reducer for the themes.

    import { SET_DARK_THEME, SET_LIGHT_THEME } from "../constants/globalConstants";
    
    export const toggleTheme = (state = {}, action) => {
      switch (action.type) {
        case SET_LIGHT_THEME:
          return {
            theme: action.payload,
            light: true
          };
        case SET_DARK_THEME:
          return {
            theme: action.payload,
            light: false
          };
        default:
          return state;
      }
    };

    This is very similar to what we’ve been doing. We used the switch statement to check the type of action and then returned the appropriate payload. We also returned a state light that determines whether light or dark theme is selected by the user. We’ll use this in our components.

    We also need to add it to our root reducer and store. Here is the complete code for our store.js.

    import { createStore, applyMiddleware } from "redux";
    import thunk from "redux-thunk";
    import { theme as initialTheme } from "../styles/definition";
    import reducers from "./reducers/index";
    
    const theme = localStorage.getItem("theme")
      ? JSON.parse(localStorage.getItem("theme"))
      : initialTheme.light;
    
    const light = localStorage.getItem("light")
      ? JSON.parse(localStorage.getItem("light"))
      : true;
    
    const initialState = {
      toggleTheme: { light, theme }
    };
    export default createStore(reducers, initialState, applyMiddleware(thunk));

    Since we needed to persist the theme when the user refreshes, we had to get it from the local storage using localStorage.getItem() and pass it to our initial state.

    Adding The Functionality To Our React Application

    Styled components provide us with ThemeProvider that allows us to pass themes through our application. We can modify our App.js file to add this functionality.

    Let’s take a look at it.

    import React from "react";
    import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
    import { useSelector } from "react-redux";
    import { ThemeProvider } from "styled-components";
    
    function App() {
      const { theme } = useSelector((state) => state.toggleTheme);
      let Theme = theme ? theme : {};
      return (
        <ThemeProvider theme={Theme}>
          <Router>
            ...
          </Router>
        </ThemeProvider>
      );
    }
    export default App;

    By passing themes through the ThemeProvider, we can easily use the theme props in our styles.

    For instance, we can set the color to our bodyText custom color as follows.

    color: ${(props) => props.theme.bodyText};

    We can use the custom themes anywhere we need color in our application.

    For example, to define border-bottom, we do the following.

    border-bottom: 2px solid ${(props) => props.theme.goldish};

    Conclusion

    We began by delving into Sanity.io, setting it up and connecting it to our React application. Then we set up Redux and used the GROQ language to query our API. We saw how to connect and use Redux to our React app using react-redux, use styled-components and theming.

    However, we only scratched the surface on what is possible with these technologies. I encourage you to go through the code samples in my GitHub repo and try your hands on a completely different project using these technologies to learn and master them.

    Resources

    Smashing Editorial
    (ks, vf, yk, il)

    Source link

    web design

    How Redux Reducers Work — Smashing Magazine

    12/14/2020

    About The Author

    Fortune Ikechi is a Frontend Engineer based in Rivers State Nigeria. He is a student of the University of Port-Harcourt. He is passionate about community and …
    More about
    Fortune

    If you have used Redux at any point while developing an application to manage state, you will most definitely have come across reducers. This tutorial will explain the concept of reducers and how they work specifically in Redux.

    In this tutorial, we’re going to learn the concept of reducers and how they work, specifically in React applications. In order to understand and better use Redux, a solid understanding of reducers is essential. Reducers provide a way to update an application’s state using an action. It is an integral part of the Redux library.

    This tutorial is for developers who want to learn more about Redux Reducers. An understanding of React and Redux would be beneficial. At the end of the tutorial, you should have a better understanding of the role Reducers play in Redux. We’ll be writing code demos and an application to better understand Reducers and how it affects the state in an application.

    What Is A Reducer

    A Reducer is a pure function that takes the state of an application and action as arguments and returns a new state. For example, an authentication reducer can take an initial state of an application in form of an empty object and an action that tells it that a user has logged in and returned a new application state with a logged-in user.

    Pure functions are functions that do not have any side effects and will return the same results if the same arguments are passed in.

    Below is an example of a pure function:

    const add = (x, y) => x + y;
    
    add(2, 5);

    The example above returns a value based on the inputs, if you pass 2 and 5 then you’d always get 10, as long as it’s the same input nothing else affects the output you get, that’s an example of a pure function.

    Below is an example of a reducer function that takes in a state and an action.

    const initialState = {};
    const cartReducer = (state = initialState, action) => {
      // Do something here
    }

    Let’s define the two parameters that a reducer takes in, state and action.

    State

    A state is the data that your component(s) is working with — it holds the data that a component requires and it dictates what a component renders. Once a state object changes, the component re-renders. If an application state is managed by Redux, then the reducer is where state changes happen.

    Action

    An action, is an object that contains the payload of information. They are the only source of information for the Redux store to be updated. Reducers update store based on the value of the action.type. Here we will define the action.type as ADD_TO_CART.

    According to the official Redux documentation, actions are the only things that trigger changes in a Redux application, they contain the payload for changes to an application store. Actions are JavaScript objects that tell Redux the type of action to be performed, usually they’re defined as functions like the one below:

    const action = {
      type: 'ADD_TO_CART',
      payload: {
        product: 'margarine',
        quantity: 4
      }
    }

    The code above is a typical payload value that contains what a user is sending and it will be used to update the state of the application. As you can see from above, the action object contains the type of action and a payload object that would be necessary for this particular action to be performed.

    Updating State Using Reducers

    To show how reducers work, let’s look at the number counter below:

    const increaseAction = {
      type: 'INCREASE',
    }; 
    
    const decreaseAction = {
      type: 'DECREASE'
    }; 
    
    
    const countReducer = (state = 0, action) => {
      switch(action.type){
      case INCREASE: 
        return state + 1;
    
      case DECREASE : 
        return state -1;
    
      default: 
     return state;
    
      }
    };

    In the code above, increaseAction and decreaseAction are actions used in the reducer to determine what the state is updated to. Next, we have a reducer function called countReducer, which takes in an action and an initial state whose value is 0. If the value of action.type is INCREASE, we return a new state that is incremented by 1, else if it is DECREASE a new state that is decremented by 1 is returned. In cases where none of those conditions are meant, we return state.

    Updating State Using Reducers: The Spread Operator

    State can’t be directly changed, to create or update state, we can use the JavaScript spread operator to make sure we don’t change the value of the state directly but instead to return a new object that contains a state passed to it and the payload of the user.

    const contactAction = {
      type: 'GET_CONTACT',
      payload: ['0801234567', '0901234567']
    };
    
    const initialState = {
      contacts: [],
      contact: {},
    };
    
    export default function (state = initialState, action) {
      switch (action.type) {
        case GET_CONTACTS: 
          return {
            ...state,
            contacts: action.payload,
        };
      default: 
        return state;
      }

    In the code above, we are using a spread operator to make sure we don’t change the state value directly, this way we can return a new object that is filled with the state that is passed to it and the payload that’s sent by the user. By using a spread operator, we can make sure that the state stays the same as we add all new items to it and also replace the contacts field in the state if it was present before.

    Redux Reducers In Action — A Demo

    To better understand Redux Reducers and how they work, we will be implementing a simple Movie details finder app, the code and working version can be found here on Codesandbox. To get started, go to your terminal and initialize a react app using the command below:

    create-react-app movie-detail-finder

    Once our project initialized, next let’s install the packages we’d need for our application.

    npm i axios reactstrap react-redux redux redux-thunk

    Once, the packages are installed, let’s start our development server using the command:

    npm start

    The above command should start our project development server in our browser. Next let’s open our project in our text editor of choice, inside our project src folder, delete the following files: App.css, App.test.js, serviceWorker.js and setupTests.js. Next, let’s delete all code that references the deleted files on our App.js.

    For this project, we will be using Open Movie Database API to get our movie information, content and images for our application, here is a link to the API, you’d need to register and get access keys in order to use it for this application, Once you’re done, let’s proceed with our application by building components.

    Building App Components

    First, inside of our src folder in our project directory, create a folder called components and inside the folder, let’s create two folders called Movie and Searchbar, our component should look like the image below:

    components folder
    Components folder. (Large preview)

    Building Movie Component

    Let’s build the Movies component, which will outline the structure of the movie details we will be getting from our API. To do this, inside the Movies folder of our component, create a new file Movie.js, next create a class based component for the API results, let’s do that below.

    import React, { Component } from 'react';
    import { Card, CardImg, CardText, CardBody, ListGroup, ListGroupItem, Badge } from 'reactstrap';
    import styles from './Movie.module.css';
    
    class Movie extends Component{
        render(){
            if(this.props.movie){
                return (
                    <div className={styles.Movie}>
                        <h3 className="text-center my-4">
                          Movie Name: {this.props.movie.Title}
                        </h3>
                        <Card className="text-primary bg-dark">
                            <CardImg className={styles.Img}
                              top src={this.props.movie.Poster} 
                              alt={this.props.movie.Title}/>
                            <CardBody>
                                <ListGroup className="bg-dark">
                                  <ListGroupItem>
                                    <Badge color="primary">Actors:</Badge>
                                      {this.props.movie.Actors}
                                      </ListGroupItem>
                              <ListGroupItem>
                                <Badge color="primary">Genre:</Badge>
                                {this.props.movie.Genre}
                              </ListGroupItem>
                              <ListGroupItem>
                                <Badge color="primary">Year:</Badge>
                                {this.props.movie.Year}
                              </ListGroupItem>
                              <ListGroupItem>
                                <Badge color="primary">Writer(s):</Badge> 
                                {this.props.movie.Writer}
                              </ListGroupItem>
                            <ListGroupItem>
                              <Badge color="primary">IMDB Rating:</Badge> 
                                {this.props.movie.imdbRating}/10
                            </ListGroupItem>
                        </ListGroup>
                          <CardText className="mt-3 text-white">
                            <Badge color="secondary">Plot:</Badge>
                              {this.props.movie.Plot}
                            </CardText>
                          </CardBody>
                        </Card>
                    </div>
                )
            }
            return null
        }
    }
    export default Movie;

    In the code above, Using components from the package reactstrap, you can check out the documentation here. We built a Card component that includes the movie name, Image, genre, actor, year, movie writer, rating, and plot. To make it easier to pass data from this component, we built data to be as props to other components. Next, let’s build our Searchbar component.

    Building Our Searchbar Component

    Our Searchbar component will feature a search bar and a button component for searching movie components, let’s do this below:

    import React from 'react';
    import styles from './Searchbar.module.css';
    import { connect } from 'react-redux';
    import { fetchMovie } from '../../actions';
    import Movie from '../Movie/Movie';
    
    class Searchbar extends React.Component{
    render(){
            return(
                <div className={styles.Form}>
                    <div>
                        <form onSubmit={this.formHandler}>
                            <input 
                                type="text" 
                                placeholder="Movie Title" 
                                onChange={e => this.setState({title: e.target.value})}
                                value={this.state.title}/>
                            <button type="submit">Search</button>
                        </form>
                    </div>
                    <Movie movie={this.props.movie}/>
                </div>
            )
        }
    }

    In the code above, we are importing connect from react-redux which is used to connect a React component to the Redux store, provides the component with information from the store and also provides functions used to dispatch actions to the store. Next, we imported the Movie component and a function fetchMovie from actions.

    Next, we have a form tag with an input box for entering our movie titles, using the setState hook from React, we added an onChange event and value that will set the state of title to the value entered in the input box. We have a button tag to search for movie titles and using the Movie component that we imported, we passed the properties of the component as props to the result of the search.

    Next for us is to write a function to submit our movie title to the API in order to send results to us, we also need to set the initial state of the application. let’s do that below.

    class Searchbar extends React.Component{
    
        state = {
            title: ''
        }
        formHandler = (event) => {
            event.preventDefault();
            this.props.fetchMovie(this.state.title);
            this.setState({title: ''});
        }
    

    Here, we set the initial state of the application to empty strings, we created a function formHandler that takes in an event parameter and passes the fetchMovie function from action and setting the title as the new state of the application. To complete our application, let’s export this component using the connect property from react-redux, to do this we’d use the react redux mapToStateProps property to select the part of the data our component would need, you can learn more about mapToStateProps here.

    const mapStateToProps = (state) => {
        return { movie: state.movie }
    }
    export default connect(mapStateToProps, { fetchMovie })(Searchbar)

    Let’s add styles to our form by creating a file Searchbar.module.css and adding the styles below:

    .Form{
      margin: 3rem auto;
      width: 80%;
      height: 100%;
    }
    input{
      display: block;
      height: 45px;
      border: none;
      width: 100%;
      border-radius: 0.5rem;
      outline: none;
      padding: 0 1rem;
    }
    input:focus, select:focus{
      border: 2px rgb(16, 204, 179) solid;
    }
    
    .Form button{
      display: block;
      background: rgb(16, 204, 179);
      padding: 0.7rem;
      border-radius: 0.5rem;
      width: 20%;
      margin-top: 0.7rem;
      color: #FFF;
      border: none;
      text-decoration: none;
      transition: all 0.5s;
    }
    button:hover{
      opacity: 0.6;
    }
    @media(max-width: 700px){
      input{
          height: 40px;
          padding: 0 1rem;
      }
      .Form button{
          width: 40%;
          padding: 0.6rem;
      }
    }

    Once we’ve done the above, our search bar component should look similar to the image below:

    Searchbar component
    Searchbar component. (Large preview)

    Creating Actions For Application

    In this component, we will be setting up Redux actions for our application, First, inside the src directory, create a folder named actions and inside the folder, we’d create an index.js file. Here we’d create a function fetchMovie that takes in a title parameter, and fetches movie from the API using Axios. Let’s do this below:

    import axios from 'axios';
    
    export const fetchMovie = (title) =>
        async (dispatch) => {
            const response = await 
              axios.get(
                `https://cors-anywhere.herokuapp.com/http://www.omdbapi.com/?t=${title}&apikey=APIKEY`);
            dispatch({
                type: 'FETCH_MOVIE',
                payload: response.data
            })
        }

    In the code above, we imported axios and created a function called fetchMovie which takes in a title parameter by using async/await so that we can make a request to the API server. We have a dispatch function that dispatches to the Redux the action object that is passed to it. From what we have above, we’re dispatching an action with the type FETCH_MOVIE and the payload that contains the response we got from the API.

    NOTE: The apikey in the request will be replaced with your own apikey after registering at OmdbAPI.

    Creating App Reducers

    In this section, we are going to create reducers for our application.

    
    const fetchMovieReducer = (state = null, action) => {
        switch(action.type){
            case 'FETCH_MOVIE':
                return action.payload;
            default:
                return state;
        }
    }
    const rootReducer = (state, action) => {
        return { 
            movie: fetchMovieReducer(state, action)
        }
    }
    export default rootReducer;

    In the code above, we created a fetchMovieReducer that takes in a default state of null and an action parameter, using a switch operator, for case FETCH_MOVIE we will return the value of the action.payload which is the movie we got from the API. If the action we tried performing isn’t in the reducer, then we return our default state.

    Next, we created a rootReducer function that will accept the current state and an action as input and returns the fetchMovieReducer.

    Putting It Together

    In this section, we’d finish our app by creating our redux store in the index.js, let’s do that below:

    import React from 'react';
    import ReactDOM from 'react-dom';
    import { Provider } from 'react-redux';
    import { createStore, applyMiddleware } from 'redux';
    import thunk from 'redux-thunk';
    import App from './App';
    import 'bootstrap/dist/css/bootstrap.min.css';
    import './index.css';
    import reducers from './reducers';
    
    const store = createStore(reducers, applyMiddleware(thunk))
    ReactDOM.render(
        <Provider store={store}>
            <>
              <App/>
            </>
        </Provider>,
        document.getElementById('root')
    )

    In the code above, we created the application store using the createStore method by passing the reducer we created and a middleware. Middlewares are addons that allow us to enhance the functionalities of Redux. Here we are making use of the Redux Thunk middleware using applyMiddleware. The Redux Thunk middleware is necessary for our store to do asynchronous updates. This is needed because by default, Redux updates the store synchronously.

    To make sure our application knows the exact store to use, we wrapped our application in a Provider component and passed the store as a prop, by doing this, other components in our application can connect and share information with the store.

    Let’s add a bit of style to our index.css file.

    *{
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    body{
      background: rgb(15, 10, 34);
      color: #FFF;
      height: 100vh;
      max-width: 100%;
    }

    Rendering And Testing A Movie Detail Finder

    In this section, we are going to conclude our application by rendering our application in our App.js, to do this, let’s create a class-based component named App and initialize our Searchbar and input field.

    import React from 'react';
    import Searchbar from './components/Searchbar/Searchbar';
    import styles from './App.module.css';
    class App extends React.Component{
        
        render(){
            return(
                <div className={styles.App}>
                    <h1 className={styles.Title}>Movies Search App</h1>
                    <Searchbar/>
                </div>
                
            )
        }
    }
    export default App;

    Here, we created an App class based component with a h1 that says Movie Search App and added our Searchbar component. Our application should look like the image below:

    movie details application with reducers
    Final movie details application using reducers. (Large preview)

    A working demo is available on Codesandbox.

    Conclusion

    Reducers are an important part of Redux state management, with reducers we can write pure functions to update specific areas of our Redux applications without side effects. We’ve learned the basics of Redux reducers, their uses, and the core concept of reducers, state, and arguments.

    You can take this further by seeing the documentation on Redux reducers here. You can take this further and build more on Redux reducers, let me know what you build.

    Resources

    Smashing Editorial
    (ks, ra, yk, il)

    Source link

    web design

    Setting Up Redux For Use In A Real-World Application — Smashing Magazine

    08/03/2020

    About The Author

    I love building software for the web, writing about web technologies, and playing video games.
    More about
    Jerry

    Redux is a robust state-management library for single-page Javascript apps. It is described on the official documentation as a predictable state container for Javascript applications and it’s fairly simple to learn the concepts and implement Redux in a simple app. Going from a simple counter app to a real-world app, however, can be quite the jump.

    Redux is an important library in the React ecosystem, and almost the default to use when working on React applications that involve state management. As such, the importance of knowing how it works cannot be overestimated.

    This guide will walk the reader through setting up Redux in a fairly complex React application and introduce the reader to “best practices” configuration along the way. It will be beneficial to beginners especially, and anyone who wants to fill in the gaps in their knowledge of Redux.

    Introducing Redux

    Redux is a library that aims to solve the problem of state management in JavaScript apps by imposing restrictions on how and when state updates can happen. These restrictions are formed from Redux’s “three principles” which are:

    • Single source of truth
      All of your application’s state is held in a Redux store. This state can be represented visually as a tree with a single ancestor, and the store provides methods for reading the current state and subscribing to changes from anywhere within your app.

    • State is read-only
      The only way to change the state is to send the data as a plain object, called an action. You can think about actions as a way of saying to the state, “I have some data I would like to insert/update/delete”.

    • Changes are made with pure functions
      To change your app’s state, you write a function that takes the previous state and an action and returns a new state object as the next state. This function is called a reducer, and it is a pure function because it returns the same output for a given set of inputs.

    The last principle is the most important in Redux, and this is where the magic of Redux happens. Reducer functions must not contain unpredictable code, or perform side-effects such as network requests, and should not directly mutate the state object.

    Redux is a great tool, as we’ll learn later in this guide, but it doesn’t come without its challenges or tradeoffs. To help make the process of writing Redux efficient and more enjoyable, the Redux team offers a toolkit that abstracts over the process of setting up a Redux store and provides helpful Redux add-ons and utilities that help to simplify application code. For example, the library uses Immer.js, a library that makes it possible for you to write “mutative” immutable update logic, under the hood.

    Recommended reading: Better Reducers With Immer

    In this guide, we will explore Redux by building an application that lets authenticated users create and manage digital diaries.

    Building Diaries.app

    As stated in the previous section, we will be taking a closer look at Redux by building an app that lets users create and manage diaries. We will be building our application using React, and we’ll set up Mirage as our API mocking server since we won’t have access to a real server in this guide.

    Starting a Project and Installing Dependencies

    Let’s get started on our project. First, bootstrap a new React application using create-react-app:

    Using npx:

    npx create-react-app diaries-app --template typescript
    

    We are starting with the TypeScript template, as we can improve our development experience by writing type-safe code.

    Now, let’s install the dependencies we’ll be needing. Navigate into your newly created project directory

    cd diaries-app
    

    And run the following commands:

    npm install --save redux react-redux @reduxjs/toolkit
    
    npm install --save axios react-router-dom react-hook-form yup dayjs markdown-to-jsx sweetalert2
    
    npm install --save-dev miragejs @types/react-redux @types/react-router-dom @types/yup @types/markdown-to-jsx
    

    The first command will install Redux, React-Redux (official React bindings for Redux), and the Redux toolkit.

    The second command installs some extra packages which will be useful for the app we’ll be building but are not required to work with Redux.

    The last command installs Mirage and type declarations for the packages we installed as devDependencies.

    Describing the Application’s Initial State

    Let’s go over our application’s requirements in detail. The application will allow authenticated users to create or modify existing diaries. Diaries are private by default, but they can be made public. Finally, diary entries will be sorted by their last modified date.

    This relationship should look something like this:

    An Overview of the Application’s Data Model. (Large preview)

    Armed with this information, we can now model our application’s state. First, we will create an interface for each of the following resources: User, Diary and DiaryEntry. Interfaces in Typescript describe the shape of an object.

    Go ahead and create a new directory named interfaces in your app’s src sub-directory:

    cd src && mkdir interfaces
    

    Next, run the following commands in the directory you just created:

    touch entry.interface.ts
    touch diary.interface.ts
    touch user.interface.ts
    

    This will create three files named entry.interface.ts, diary.interface.ts and user.interface.ts respectively. I prefer to keep interfaces that would be used in multiple places across my app in a single location.

    Open entry.interface.ts and add the following code to set up the Entry interface:

    export interface Entry {
      id?: string;
      title: string;
      content: string;
      createdAt?: string;
      updatedAt?: string;
      diaryId?: string;
    }
    

    A typical diary entry will have a title and some content, as well as information about when it was created or last updated. We’ll get back to the diaryId property later.

    Next, add the following to diary.interface.ts:

    export interface Diary {
      id?: string;
      title: string;
      type: 'private' | 'public';
      createdAt?: string;
      updatedAt?: string;
      userId?: string;
      entryIds: string[] | null;
    }
    

    Here, we have a type property which expects an exact value of either ‘private’ or ‘public’, as diaries must be either private or public. Any other value will throw an error in the TypeScript compiler.

    We can now describe our User object in the user.interface.ts file as follows:

    export interface User {
      id?: string;
      username: string;
      email: string;
      password?: string;
      diaryIds: string[] | null;
    }
    

    With our type definitions finished and ready to be used across our app, let’s setup our mock API server using Mirage.

    Setting up API Mocking with MirageJS

    Since this tutorial is focused on Redux, we will not go into the details of setting up and using Mirage in this section. Please check out this excellent series if you would like to learn more about Mirage.

    To get started, navigate to your src directory and create a file named server.ts by running the following commands:

    mkdir -p services/mirage
    cd services/mirage
    
    # ~/diaries-app/src/services/mirage
    touch server.ts
    

    Next, open the server.ts file and add the following code:

    import { Server, Model, Factory, belongsTo, hasMany, Response } from 'miragejs';
    
    export const handleErrors = (error: any, message = 'An error ocurred') => {
      return new Response(400, undefined, {
        data: {
          message,
          isError: true,
        },
      });
    };
    
    export const setupServer = (env?: string): Server => {
      return new Server({
        environment: env ?? 'development',
    
        models: {
          entry: Model.extend({
            diary: belongsTo(),
          }),
          diary: Model.extend({
            entry: hasMany(),
            user: belongsTo(),
          }),
          user: Model.extend({
            diary: hasMany(),
          }),
        },
    
        factories: {
          user: Factory.extend({
            username: 'test',
            password: 'password',
            email: 'test@email.com',
          }),
        },
    
        seeds: (server): any => {
          server.create('user');
        },
    
        routes(): void {
          this.urlPrefix = 'https://diaries.app';
        },
      });
    };
    

    In this file, we are exporting two functions. A utility function for handling errors, and setupServer(), which returns a new server instance. The setupServer() function takes an optional argument which can be used to change the server’s environment. You can use this to set up Mirage for testing later.

    We have also defined three models in the server’s models property: User, Diary and Entry. Remember that earlier we set up the Entry interface with a property named diaryId. This value will be automatically set to the id the entry is being saved to. Mirage uses this property to establish a relationship between an Entry and a Diary. The same thing also happens when a user creates a new diary: userId is automatically set to that user’s id.

    We seeded the database with a default user and configured Mirage to intercept all requests from our app starting with https://diaries.app. Notice that we haven’t configured any route handlers yet. Let’s go ahead and create a few.

    Ensure that you are in the src/services/mirage directory, then create a new directory named routes using the following command:

    # ~/diaries-app/src/services/mirage
    mkdir routes
    

    cd to the newly created directory and create a file named user.ts:

    cd routes
    touch user.ts
    

    Next, paste the following code in the user.ts file:

    import { Response, Request } from 'miragejs';
    import { handleErrors } from '../server';
    import { User } from '../../../interfaces/user.interface';
    import { randomBytes } from 'crypto';
    
    const generateToken = () => randomBytes(8).toString('hex');
    
    export interface AuthResponse {
      token: string;
      user: User;
    }
    
    const login = (schema: any, req: Request): AuthResponse | Response => {
      const { username, password } = JSON.parse(req.requestBody);
      const user = schema.users.findBy({ username });
      if (!user) {
        return handleErrors(null, 'No user with that username exists');
      }
      if (password !== user.password) {
        return handleErrors(null, 'Password is incorrect');
      }
      const token = generateToken();
      return {
        user: user.attrs as User,
        token,
      };
    };
    
    const signup = (schema: any, req: Request): AuthResponse | Response => {
      const data = JSON.parse(req.requestBody);
      const exUser = schema.users.findBy({ username: data.username });
      if (exUser) {
        return handleErrors(null, 'A user with that username already exists.');
      }
      const user = schema.users.create(data);
      const token = generateToken();
      return {
        user: user.attrs as User,
        token,
      };
    };
    
    export default {
      login,
      signup,
    };
    

    The login and signup methods here receive a Schema class and a fake Request object and, upon validating the password or checking that the login does not already exist, return the existing user or a new user respectively. We use the Schema object to interact with Mirage’s ORM, while the Request object contains information about the intercepted request including the request body and headers.

    Next, let’s add methods for working with diaries and diary entries. Create a file named diary.ts in your routes directory:

    touch diary.ts
    

    Update the file with the following methods for working with Diary resources:

    export const create = (
      schema: any,
      req: Request
    ): { user: User; diary: Diary } | Response => {
      try {
        const { title, type, userId } = JSON.parse(req.requestBody) as Partial<
          Diary
        >;
        const exUser = schema.users.findBy({ id: userId });
        if (!exUser) {
          return handleErrors(null, 'No such user exists.');
        }
        const now = dayjs().format();
        const diary = exUser.createDiary({
          title,
          type,
          createdAt: now,
          updatedAt: now,
        });
        return {
          user: {
            ...exUser.attrs,
          },
          diary: diary.attrs,
        };
      } catch (error) {
        return handleErrors(error, 'Failed to create Diary.');
      }
    };
    
    export const updateDiary = (schema: any, req: Request): Diary | Response => {
      try {
        const diary = schema.diaries.find(req.params.id);
        const data = JSON.parse(req.requestBody) as Partial<Diary>;
        const now = dayjs().format();
        diary.update({
          ...data,
          updatedAt: now,
        });
        return diary.attrs as Diary;
      } catch (error) {
        return handleErrors(error, 'Failed to update Diary.');
      }
    };
    
    export const getDiaries = (schema: any, req: Request): Diary[] | Response => {
      try {
        const user = schema.users.find(req.params.id);
        return user.diary as Diary[];
      } catch (error) {
        return handleErrors(error, 'Could not get user diaries.');
      }
    };
    

    Next, let’s add some methods for working with diary entries:

    export const addEntry = (
      schema: any,
      req: Request
    ): { diary: Diary; entry: Entry } | Response => {
      try {
        const diary = schema.diaries.find(req.params.id);
        const { title, content } = JSON.parse(req.requestBody) as Partial<Entry>;
        const now = dayjs().format();
        const entry = diary.createEntry({
          title,
          content,
          createdAt: now,
          updatedAt: now,
        });
        diary.update({
          ...diary.attrs,
          updatedAt: now,
        });
        return {
          diary: diary.attrs,
          entry: entry.attrs,
        };
      } catch (error) {
        return handleErrors(error, 'Failed to save entry.');
      }
    };
    
    export const getEntries = (
      schema: any,
      req: Request
    ): { entries: Entry[] } | Response => {
      try {
        const diary = schema.diaries.find(req.params.id);
        return diary.entry;
      } catch (error) {
        return handleErrors(error, 'Failed to get Diary entries.');
      }
    };
    
    export const updateEntry = (schema: any, req: Request): Entry | Response => {
      try {
        const entry = schema.entries.find(req.params.id);
        const data = JSON.parse(req.requestBody) as Partial<Entry>;
        const now = dayjs().format();
        entry.update({
          ...data,
          updatedAt: now,
        });
        return entry.attrs as Entry;
      } catch (error) {
        return handleErrors(error, 'Failed to update entry.');
      }
    };
    

    Finally, let’s add the necessary imports at the top of the file:

    import { Response, Request } from 'miragejs';
    import { handleErrors } from '../server';
    import { Diary } from '../../../interfaces/diary.interface';
    import { Entry } from '../../../interfaces/entry.interface';
    import dayjs from 'dayjs';
    import { User } from '../../../interfaces/user.interface';
    

    In this file, we have exported methods for working with the Diary and Entry models. In the create method, we call a method named user.createDiary() to save a new diary and associate it to a user account.

    The addEntry and updateEntry methods create and correctly associate a new entry to a diary or update an existing entry’s data respectively. The latter also updates the entry’s updatedAt property with the current timestamp. The updateDiary method also updates a diary with the timestamp the change was made. Later, we’ll be sorting the records we receive from our network request with this property.

    We also have a getDiaries method which retrieves a user’s diaries and a getEntries methods which retrieves a selected diary’s entries.

    We can now update our server to use the methods we just created. Open server.ts to include the files:

    import { Server, Model, Factory, belongsTo, hasMany, Response } from 'miragejs';
    
    import user from './routes/user';
    import * as diary from './routes/diary';
    

    Then, update the server’s route property with the routes we want to handle:

    export const setupServer = (env?: string): Server => {
      return new Server({
        // ...
        routes(): void {
          this.urlPrefix = 'https://diaries.app';
    
          this.get('/diaries/entries/:id', diary.getEntries);
          this.get('/diaries/:id', diary.getDiaries);
    
          this.post('/auth/login', user.login);
          this.post('/auth/signup', user.signup);
    
          this.post('/diaries/', diary.create);
          this.post('/diaries/entry/:id', diary.addEntry);
    
          this.put('/diaries/entry/:id', diary.updateEntry);
          this.put('/diaries/:id', diary.updateDiary);
        },
      });
    };
    

    With this change, when a network request from our app matches one of the route handlers, Mirage intercepts the request and invokes the respective route handler functions.

    Next, we’ll proceed to make our application aware of the server. Open src/index.tsx and import the setupServer() method:

    import { setupServer } from './services/mirage/server';
    

    And add the following code before ReactDOM.render():

    if (process.env.NODE_ENV === 'development') {
      setupServer();
    }
    

    The check in the code block above ensures that our Mirage server will run only while we are in development mode.

    One last thing we need to do before moving on to the Redux bits is configure a custom Axios instance for use in our app. This will help to reduce the amount of code we’ll have to write later on.

    Create a file named api.ts under src/services and add the following code to it:

    import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios';
    import { showAlert } from '../util';
    
    const http: AxiosInstance = axios.create({
      baseURL: 'https://diaries.app',
    });
    
    http.defaults.headers.post['Content-Type'] = 'application/json';
    
    http.interceptors.response.use(
      async (response: AxiosResponse): Promise => {
        if (response.status >= 200 && response.status < 300) {
          return response.data;
        }
      },
      (error: AxiosError) => {
        const { response, request }: {
          response?: AxiosResponse;
          request?: XMLHttpRequest;
        } = error;
        if (response) {
          if (response.status >= 400 && response.status < 500) {
            showAlert(response.data?.data?.message, 'error');
            return null;
          }
        } else if (request) {
          showAlert('Request failed. Please try again.', 'error');
          return null;
        }
        return Promise.reject(error);
      }
    );
    
    export default http;
    

    In this file, we are exporting an Axios instance modified to include our app’s API url, https://diaries.app. We have configured an interceptor to handle success and error responses, and we display error messages using a sweetalert toast which we will configure in the next step.

    Create a file named util.ts in your src directory and paste the following code in it:

    import Swal, { SweetAlertIcon } from 'sweetalert2';
    
    export const showAlert = (titleText = 'Something happened.', alertType?: SweetAlertIcon): void => {
      Swal.fire({
        titleText,
        position: 'top-end',
        timer: 3000,
        timerProgressBar: true,
        toast: true,
        showConfirmButton: false,
        showCancelButton: true,
        cancelButtonText: 'Dismiss',
        icon: alertType,
        showClass: {
          popup: 'swal2-noanimation',
          backdrop: 'swal2-noanimation',
        },
        hideClass: {
          popup: '',
          backdrop: '',
        },
      });
    };
    

    This file exports a function that displays a toast whenever it is invoked. The function accepts parameters to allow you set the toast message and type. For example, we are showing an error toast in the Axios response error interceptor like this:

    showAlert(response.data?.data?.message, 'error');
    

    Now when we make requests from our app while in development mode, they will be intercepted and handled by Mirage instead. In the next section, we will set up our Redux store using Redux toolkit.

    Setting up a Redux Store

    In this section, we are going to set up our store using the following exports from Redux toolkit: configureStore(), getDefaultMiddleware() and createSlice(). Before we start, we should take a detailed look at what these exports do.

    configureStore() is an abstraction over the Redux createStore() function that helps simplify your code. It uses createStore() internally to set up your store with some useful development tools:

    export const store = configureStore({
      reducer: rootReducer, // a single reducer function or an object of slice reducers
    });
    

    The createSlice() function helps simplify the process of creating action creators and slice reducers. It accepts an initial state, an object full of reducer functions, and a “slice name”, and automatically generates action creators and action types corresponding to the reducers and your state. It also returns a single reducer function, which can be passed to Redux’s combineReducers() function as a “slice reducer”.

    Remember that the state is a single tree, and a single root reducer manages changes to that tree. For maintainability, it is recommended to split your root reducer into “slices,” and have a “slice reducer” provide an initial value and calculate the updates to a corresponding slice of the state. These slices can be joined into a single reducer function by using combineReducers().

    There are additional options for configuring the store. For example, you can pass an array of your own middleware to configureStore() or start up your app from a saved state using the preloadedState option. When you supply the middleware option, you have to define all the middleware you want added to the store. If you would like to retain the defaults when setting up your store, you can use getDefaultMiddleware() to get the default list of middleware:

    export const store = configureStore({
      // ...
      middleware: [...getDefaultMiddleware(), customMiddleware],
    });
    

    Let’s now proceed to set up our store. We will adopt a “ducks-style” approach to structuring our files, specifically following the guidelines in practice from the Github Issues sample app. We will be organizing our code such that related components, as well as actions and reducers, live in the same directory. The final state object will look like this:

    type RootState = {
      auth: {
        token: string | null;
        isAuthenticated: boolean;
      };
      diaries: Diary[];
      entries: Entry[];
      user: User | null;
      editor: {
        canEdit: boolean;
        currentlyEditing: Entry | null;
        activeDiaryId: string | null;
      };
    }
    

    To get started, create a new directory named features under your src directory:

    # ~/diaries-app/src
    mkdir features
    

    Then, cd into features and create directories named auth, diary and entry:

    cd features
    mkdir auth diary entry
    

    cd into the auth directory and create a file named authSlice.ts:

    cd auth
    # ~/diaries-app/src/features/auth
    touch authSlice.ts
    

    Open the file and paste the following in it:

    import { createSlice, PayloadAction } from '@reduxjs/toolkit';
    
    interface AuthState {
      token: string | null;
      isAuthenticated: boolean;
    }
    
    const initialState: AuthState = {
      token: null,
      isAuthenticated: false,
    };
    
    const auth = createSlice({
      name: 'auth',
      initialState,
      reducers: {
        saveToken(state, { payload }: PayloadAction) {
          if (payload) {
            state.token = payload;
          }
        },
        clearToken(state) {
          state.token = null;
        },
        setAuthState(state, { payload }: PayloadAction) {
          state.isAuthenticated = payload;
        },
      },
    });
    
    export const { saveToken, clearToken, setAuthState } = auth.actions;
    export default auth.reducer;
    

    In this file, we’re creating a slice for the auth property of our app’s state using the createSlice() function introduced earlier. The reducers property holds a map of reducer functions for updating values in the auth slice. The returned object contains automatically generated action creators and a single slice reducer. We would need to use these in other files so, following the “ducks pattern”, we do named exports of the action creators, and a default export of the reducer function.

    Let’s set up the remaining reducer slices according to the app state we saw earlier. First, create a file named userSlice.ts in the auth directory and add the following code to it:

    import { createSlice, PayloadAction } from '@reduxjs/toolkit';
    import { User } from '../../interfaces/user.interface';
    
    const user = createSlice({
      name: 'user',
      initialState: null as User | null,
      reducers: {
        setUser(state, { payload }: PayloadAction<User | null>) {
          return state = (payload != null) ? payload : null;
        },
      },
    });
    
    export const { setUser } = user.actions;
    export default user.reducer;
    

    This creates a slice reducer for the user property in our the application’s store. The setUser reducer function accepts a payload containing user data and updates the state with it. When no data is passed, we set the state’s user property to null.

    Next, create a file named diariesSlice.ts under src/features/diary:

    # ~/diaries-app/src/features
    cd diary
    touch diariesSlice.ts
    

    Add the following code to the file:

    import { createSlice, PayloadAction } from '@reduxjs/toolkit';
    import { Diary } from '../../interfaces/diary.interface';
    
    const diaries = createSlice({
      name: 'diaries',
      initialState: [] as Diary[],
      reducers: {
        addDiary(state, { payload }: PayloadAction<Diary[]>) {
          const diariesToSave = payload.filter((diary) => {
            return state.findIndex((item) => item.id === diary.id) === -1;
          });
          state.push(...diariesToSave);
        },
        updateDiary(state, { payload }: PayloadAction<Diary>) {
          const { id } = payload;
          const diaryIndex = state.findIndex((diary) => diary.id === id);
          if (diaryIndex !== -1) {
            state.splice(diaryIndex, 1, payload);
          }
        },
      },
    });
    
    export const { addDiary, updateDiary } = diaries.actions;
    export default diaries.reducer;
    

    The “diaries” property of our state is an array containing the user’s diaries, so our reducer functions here all work on the state object they receive using array methods. Notice here that we are writing normal “mutative” code when working on the state. This is possible because the reducer functions we create using the createSlice() method are wrapped with Immer’s produce() method. This results in Immer returning a correct immutably updated result for our state regardless of us writing mutative code.

    Next, create a file named entriesSlice.ts under src/features/entry:

    # ~/diaries-app/src/features
    mkdir entry
    cd entry
    touch entriesSlice.ts
    

    Open the file and add the following code:

    import { createSlice, PayloadAction } from '@reduxjs/toolkit';
    import { Entry } from '../../interfaces/entry.interface';
    
    const entries = createSlice({
      name: 'entries',
      initialState: [] as Entry[],
      reducers: {
        setEntries(state, { payload }: PayloadAction<Entry[] | null>) {
          return (state = payload != null ? payload : []);
        },
        updateEntry(state, { payload }: PayloadAction<Entry>) {
          const { id } = payload;
          const index = state.findIndex((e) => e.id === id);
          if (index !== -1) {
            state.splice(index, 1, payload);
          }
        },
      },
    });
    
    export const { setEntries, updateEntry } = entries.actions;
    export default entries.reducer;
    

    The reducer functions here have logic similar to the previous slice’s reducer functions. The entries property is also an array, but it only holds entries for a single diary. In our app, this will be the diary currently in the user’s focus.

    Finally, create a file named editorSlice.ts in src/features/entry and add the following to it:

    import { createSlice, PayloadAction } from '@reduxjs/toolkit';
    import { Entry } from '../../interfaces/entry.interface';
    
    interface EditorState {
      canEdit: boolean;
      currentlyEditing: Entry | null;
      activeDiaryId: string | null;
    }
    
    const initialState: EditorState = {
      canEdit: false,
      currentlyEditing: null,
      activeDiaryId: null,
    };
    
    const editor = createSlice({
      name: 'editor',
      initialState,
      reducers: {
        setCanEdit(state, { payload }: PayloadAction<boolean>) {
          state.canEdit = payload != null ? payload : !state.canEdit;
        },
        setCurrentlyEditing(state, { payload }: PayloadAction<Entry | null>) {
          state.currentlyEditing = payload;
        },
        setActiveDiaryId(state, { payload }: PayloadAction<string>) {
          state.activeDiaryId = payload;
        },
      },
    });
    
    export const { setCanEdit, setCurrentlyEditing, setActiveDiaryId } = editor.actions;
    export default editor.reducer;
    

    Here, we have a slice for the editor property in state. We’ll be using the properties in this object to check if the user wants to switch to editing mode, which diary the edited entry belongs to, and what entry is going to be edited.

    To put it all together, create a file named rootReducer.ts in the src directory with the following content:

    import { combineReducers } from '@reduxjs/toolkit';
    import authReducer from './features/auth/authSlice';
    import userReducer from './features/auth/userSlice';
    import diariesReducer from './features/diary/diariesSlice';
    import entriesReducer from './features/entry/entriesSlice';
    import editorReducer from './features/entry/editorSlice';
    
    const rootReducer = combineReducers({
      auth: authReducer,
      diaries: diariesReducer,
      entries: entriesReducer,
      user: userReducer,
      editor: editorReducer,
    });
    
    export type RootState = ReturnType<typeof rootReducer>;
    export default rootReducer;
    

    In this file, we’ve combined our slice reducers into a single root reducer with the combineReducers() function. We’ve also exported the RootState type, which will be useful later when we’re selecting values from the store. We can now use the root reducer (the default export of this file) to set up our store.

    Create a file named store.ts with the following contents:

    import { configureStore } from '@reduxjs/toolkit';
    import rootReducer from './rootReducer';
    import { useDispatch } from 'react-redux';
    
    const store = configureStore({
      reducer: rootReducer,
    });
    
    type AppDispatch = typeof store.dispatch;
    export const useAppDispatch = () => useDispatch<AppDispatch>();
    export default store;
    

    With this, we’ve created a store using the configureStore() export from Redux toolkit. We’ve also exported an hook called useAppDispatch() which merely returns a typed useDispatch() hook.

    Next, update the imports in index.tsx to look like the following:

    import React from 'react';
    import ReactDOM from 'react-dom';
    import './index.css';
    import App from './app/App';
    import * as serviceWorker from './serviceWorker';
    import { setupServer } from './services/mirage/server';
    import { Provider } from 'react-redux';
    import store from './store';
    // ...
    

    Finally, make the store available to the app’s components by wrapping <App /> (the top-level component) with <Provider />:

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

    Now, if you start your app and you navigate to http://localhost:3000 with the Redux Dev Tools extension enabled, you should see the following in your app’s state:

    Initial State in Redux Dev Tools Extension. (Large preview)

    Great work so far, but we’re not quite finished yet. In the next section, we will design the app’s User Interface and add functionality using the store we’ve just created.

    Designing The Application User Interface

    To see Redux in action, we are going to build a demo app. In this section, we will connect our components to the store we’ve created and learn to dispatch actions and modify the state using reducer functions. We will also learn how to read values from the store. Here’s what our Redux-powered application will look like.

    Home page showing an authenticated user’s diaries. (Large preview)
    Screenshots of final app. (Large preview)

    Setting up the Authentication Feature

    To get started, move App.tsx and its related files from the src directory to its own directory like this:

    # ~/diaries-app/src
    mkdir app
    mv App.tsx App.test.tsx app
    

    You can delete the App.css and logo.svg files as we won’t be needing them.

    Next, open the App.tsx file and replace its contents with the following:

    import React, { FC, lazy, Suspense } from 'react';
    import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
    import { useSelector } from 'react-redux';
    import { RootState } from '../rootReducer';
    
    const Auth = lazy(() => import('../features/auth/Auth'));
    const Home = lazy(() => import('../features/home/Home'));
    
    const App: FC = () => {
      const isLoggedIn = useSelector(
        (state: RootState) => state.auth.isAuthenticated
      );
      return (
        <Router>
          <Switch>
            <Route path="/">
              <Suspense fallback={<p>Loading...</p>}>
                {isLoggedIn ? <Home /> : <Auth />}
              </Suspense>
            </Route>
          </Switch>
        </Router>
      );
    };
    
    export default App;
    

    Here we have set up our app to render an <Auth /> component if the user is unauthenticated, or otherwise render a <Home /> component. We haven’t created either of these components yet, so let’s fix that. Create a file named Auth.tsx under src/features/auth and add the following contents to the file:

    import React, { FC, useState } from 'react';
    import { useForm } from 'react-hook-form';
    import { User } from '../../interfaces/user.interface';
    import * as Yup from 'yup';
    import http from '../../services/api';
    import { saveToken, setAuthState } from './authSlice';
    import { setUser } from './userSlice';
    import { AuthResponse } from '../../services/mirage/routes/user';
    import { useAppDispatch } from '../../store';
    
    const schema = Yup.object().shape({
      username: Yup.string()
        .required('What? No username?')
        .max(16, 'Username cannot be longer than 16 characters'),
      password: Yup.string().required('Without a password, "None shall pass!"'),
      email: Yup.string().email('Please provide a valid email address (abc@xy.z)'),
    });
    
    const Auth: FC = () => {
      const { handleSubmit, register, errors } = useForm<User>({
        validationSchema: schema,
      });
      const [isLogin, setIsLogin] = useState(true);
      const [loading, setLoading] = useState(false);
      const dispatch = useAppDispatch();
    
      const submitForm = (data: User) => {
        const path = isLogin ? '/auth/login' : '/auth/signup';
        http
          .post<User, AuthResponse>(path, data)
          .then((res) => {
            if (res) {
              const { user, token } = res;
              dispatch(saveToken(token));
              dispatch(setUser(user));
              dispatch(setAuthState(true));
            }
          })
          .catch((error) => {
            console.log(error);
          })
          .finally(() => {
            setLoading(false);
          });
      };
    
      return (
        <div className="auth">
          <div className="card">
            <form onSubmit={handleSubmit(submitForm)}>
              <div className="inputWrapper">
                <input ref={register} name="username" placeholder="Username" />
                {errors && errors.username && (
                  <p className="error">{errors.username.message}</p>
                )}
              </div>
              <div className="inputWrapper">
                <input
                  ref={register}
                  name="password"
                  type="password"
                  placeholder="Password"
                />
                {errors && errors.password && (
                  <p className="error">{errors.password.message}</p>
                )}
              </div>
              {!isLogin && (
                <div className="inputWrapper">
                  <input
                    ref={register}
                    name="email"
                    placeholder="Email (optional)"
                  />
                  {errors && errors.email && (
                    <p className="error">{errors.email.message}</p>
                  )}
                </div>
              )}
              <div className="inputWrapper">
                <button type="submit" disabled={loading}>
                  {isLogin ? 'Login' : 'Create account'}
                </button>
              </div>
              <p
                onClick={() => setIsLogin(!isLogin)}
                style={{ cursor: 'pointer', opacity: 0.7 }}
              >
                {isLogin ? 'No account? Create one' : 'Already have an account?'}
              </p>
            </form>
          </div>
        </div>
      );
    };
    
    export default Auth;
    

    In this component, we have set up a form for users to log in, or to create an account. Our form fields are validated using Yup and, on successfully authenticating a user, we use our useAppDispatch hook to dispatch the relevant actions. You can see the dispatched actions and the changes made to your state in the Redux DevTools Extension:

    Dispatched Actions with Changes Tracked in Redux Dev Tools Extensions. (Large preview)

    Finally, create a file named Home.tsx under src/features/home and add the following code to the file:

    import React, { FC } from 'react';
    
    const Home: FC = () => {
      return (
        <div>
          <p>Welcome user!</p>
        </div>
      );
    };
    
    export default Home;
    

    For now, we are just displaying some text to the authenticated user. As we build the rest of our application, we will be updating this file.

    Setting up the Editor

    The next component we are going to build is the editor. Though basic, we will enable support for rendering markdown content using the markdown-to-jsx library we installed earlier.

    First, create a file named Editor.tsx in the src/features/entry directory. Then, add the following code to the file:

    import React, { FC, useState, useEffect } from 'react';
    import { useSelector } from 'react-redux';
    import { RootState } from '../../rootReducer';
    import Markdown from 'markdown-to-jsx';
    import http from '../../services/api';
    import { Entry } from '../../interfaces/entry.interface';
    import { Diary } from '../../interfaces/diary.interface';
    import { setCurrentlyEditing, setCanEdit } from './editorSlice';
    import { updateDiary } from '../diary/diariesSlice';
    import { updateEntry } from './entriesSlice';
    import { showAlert } from '../../util';
    import { useAppDispatch } from '../../store';
    
    const Editor: FC = () => {
      const { currentlyEditing: entry, canEdit, activeDiaryId } = useSelector(
        (state: RootState) => state.editor
      );
      const [editedEntry, updateEditedEntry] = useState(entry);
      const dispatch = useAppDispatch();
    
      const saveEntry = async () => {
        if (activeDiaryId == null) {
          return showAlert('Please select a diary.', 'warning');
        }
        if (entry == null) {
          http
            .post<Entry, { diary: Diary; entry: Entry }>(
              `/diaries/entry/${activeDiaryId}`,
              editedEntry
            )
            .then((data) => {
              if (data != null) {
                const { diary, entry: _entry } = data;
                dispatch(setCurrentlyEditing(_entry));
                dispatch(updateDiary(diary));
              }
            });
        } else {
          http
            .put<Entry, Entry>(`diaries/entry/${entry.id}`, editedEntry)
            .then((_entry) => {
              if (_entry != null) {
                dispatch(setCurrentlyEditing(_entry));
                dispatch(updateEntry(_entry));
              }
            });
        }
        dispatch(setCanEdit(false));
      };
    
      useEffect(() => {
        updateEditedEntry(entry);
      }, [entry]);
    
      return (
        <div className="editor">
          <header
            style={{
              display: 'flex',
              flexWrap: 'wrap',
              alignItems: 'center',
              marginBottom: '0.2em',
              paddingBottom: '0.2em',
              borderBottom: '1px solid rgba(0,0,0,0.1)',
            }}
          >
            {entry && !canEdit ? (
              <h4>
                {entry.title}
                <a
                  href="#edit"
                  onClick={(e) => {
                    e.preventDefault();
                    if (entry != null) {
                      dispatch(setCanEdit(true));
                    }
                  }}
                  style={{ marginLeft: '0.4em' }}
                >
                  (Edit)
                </a>
              </h4>
            ) : (
              <input
                value={editedEntry?.title ?? ''}
                disabled={!canEdit}
                onChange={(e) => {
                  if (editedEntry) {
                    updateEditedEntry({
                      ...editedEntry,
                      title: e.target.value,
                    });
                  } else {
                    updateEditedEntry({
                      title: e.target.value,
                      content: '',
                    });
                  }
                }}
              />
            )}
          </header>
          {entry && !canEdit ? (
            <Markdown>{entry.content}</Markdown>
          ) : (
            <>
              <textarea
                disabled={!canEdit}
                placeholder="Supports markdown!"
                value={editedEntry?.content ?? ''}
                onChange={(e) => {
                  if (editedEntry) {
                    updateEditedEntry({
                      ...editedEntry,
                      content: e.target.value,
                    });
                  } else {
                    updateEditedEntry({
                      title: '',
                      content: e.target.value,
                    });
                  }
                }}
              />
              <button onClick={saveEntry} disabled={!canEdit}>
                Save
              </button>
            </>
          )}
        </div>
      );
    };
    
    export default Editor;
    

    Let’s break down what’s happening in the Editor component.

    First, we are picking some values (with correctly inferred types) from the app’s state using the useSelector() hook from react-redux. In the next line, we have a stateful value called editedEntry whose initial value is set to the editor.currentlyEditing property we’ve selected from the store.

    Next, we have the saveEntry function which updates or creates a new entry in the API, and dispatches the respective Redux action.

    Finally, we have a useEffect that is fired when the editor.currentlyEditing property changes. Our editor’s UI (in the component’s return function) has been set up to respond to changes in the state. For example, rendering the entry’s content as JSX elements when the user isn’t editing.

    With that, the app’s Entry feature should be completely set up. In the next section, we will finish building the Diary feature and then import the main components in the Home component we created earlier.

    Final Steps

    To finish up our app, we will first create components for the Diary feature. Then, we will update the Home component with the primary exports from the Diary and Entry features. Finally, we will add some styling to give our app the required pizzazz!

    Let’s start by creating a file in src/features/diary named DiaryTile.tsx. This component will present information about a diary and its entries, and allow the user to edit the diary’s title. Add the following code to the file:

    import React, { FC, useState } from 'react';
    import { Diary } from '../../interfaces/diary.interface';
    import http from '../../services/api';
    import { updateDiary } from './diariesSlice';
    import { setCanEdit, setActiveDiaryId, setCurrentlyEditing } from '../entry/editorSlice';
    import { showAlert } from '../../util';
    import { Link } from 'react-router-dom';
    import { useAppDispatch } from '../../store';
    
    interface Props {
      diary: Diary;
    }
    
    const buttonStyle: React.CSSProperties = {
      fontSize: '0.7em',
      margin: '0 0.5em',
    };
    
    const DiaryTile: FC<Props> = (props) => {
      const [diary, setDiary] = useState(props.diary);
      const [isEditing, setIsEditing] = useState(false);
      const dispatch = useAppDispatch();
      const totalEntries = props.diary?.entryIds?.length;
    
      const saveChanges = () => {
        http
          .put<Diary, Diary>(`/diaries/${diary.id}`, diary)
          .then((diary) => {
            if (diary) {
              dispatch(updateDiary(diary));
              showAlert('Saved!', 'success');
            }
          })
          .finally(() => {
            setIsEditing(false);
          });
      };
    
      return (
        <div className="diary-tile">
          <h2
            className="title"
            title="Click to edit"
            onClick={() => setIsEditing(true)}
            style={{
              cursor: 'pointer',
            }}
          >
            {isEditing ? (
              <input
                value={diary.title}
                onChange={(e) => {
                  setDiary({
                    ...diary,
                    title: e.target.value,
                  });
                }}
                onKeyUp={(e) => {
                  if (e.key === 'Enter') {
                    saveChanges();
                  }
                }}
              />
            ) : (
              <span>{diary.title}</span>
            )}
          </h2>
          <p className="subtitle">{totalEntries ?? '0'} saved entries</p>
          <div style={{ display: 'flex' }}>
            <button
              style={buttonStyle}
              onClick={() => {
                dispatch(setCanEdit(true));
                dispatch(setActiveDiaryId(diary.id as string));
                dispatch(setCurrentlyEditing(null));
              }}
            >
              Add New Entry
            </button>
            <Link to={`diary/${diary.id}`} style={{ width: '100%' }}>
              <button className="secondary" style={buttonStyle}>
                View all →
              </button>
            </Link>
          </div>
        </div>
      );
    };
    
    export default DiaryTile;
    

    In this file, we receive a diary object as a prop and display the data in our component. Notice that we use local state and component props for our data display here. That’s because you don’t have to manage all your app’s state using Redux. Sharing data using props, and maintaining local state in your components is acceptable and encouraged in some cases.

    Next, let’s create a component that will display a list of a diary’s entries, with the last updated entries at the top of the list. Ensure you are in the src/features/diary directory, then create a file named DiaryEntriesList.tsx and add the following code to the file:

    import React, { FC, useEffect } from 'react';
    import { useParams, Link } from 'react-router-dom';
    import { useSelector } from 'react-redux';
    import { RootState } from '../../rootReducer';
    import http from '../../services/api';
    import { Entry } from '../../interfaces/entry.interface';
    import { setEntries } from '../entry/entriesSlice';
    import { setCurrentlyEditing, setCanEdit } from '../entry/editorSlice';
    import dayjs from 'dayjs';
    import { useAppDispatch } from '../../store';
    
    const DiaryEntriesList: FC = () => {
      const { entries } = useSelector((state: RootState) => state);
      const dispatch = useAppDispatch();
      const { id } = useParams();
    
      useEffect(() => {
        if (id != null) {
          http
            .get<null, { entries: Entry[] }>(`/diaries/entries/${id}`)
            .then(({ entries: _entries }) => {
              if (_entries) {
                const sortByLastUpdated = _entries.sort((a, b) => {
                  return dayjs(b.updatedAt).unix() - dayjs(a.updatedAt).unix();
                });
                dispatch(setEntries(sortByLastUpdated));
              }
            });
        }
      }, [id, dispatch]);
    
      return (
        <div className="entries">
          <header>
            <Link to="/">
              <h3>← Go Back</h3>
            </Link>
          </header>
          <ul>
            {entries.map((entry) => (
              <li
                key={entry.id}
                onClick={() => {
                  dispatch(setCurrentlyEditing(entry));
                  dispatch(setCanEdit(true));
                }}
              >
                {entry.title}
              </li>
            ))}
          </ul>
        </div>
      );
    };
    
    export default DiaryEntriesList;
    

    Here, we subscribe to the entries property of our app’s state, and have our effect fetch a diary’s entry only run when a property, id, changes. This property’s value is gotten from our URL as a path parameter using the useParams() hook from react-router. In the next step, we will create a component that will enable users to create and view diaries, as well as render a diary’s entries when it is in focus.

    Create a file named Diaries.tsx while still in the same directory, and add the following code to the file:

    import React, { FC, useEffect } from 'react';
    import { useSelector } from 'react-redux';
    import { RootState } from '../../rootReducer';
    import http from '../../services/api';
    import { Diary } from '../../interfaces/diary.interface';
    import { addDiary } from './diariesSlice';
    import Swal from 'sweetalert2';
    import { setUser } from '../auth/userSlice';
    import DiaryTile from './DiaryTile';
    import { User } from '../../interfaces/user.interface';
    import { Route, Switch } from 'react-router-dom';
    import DiaryEntriesList from './DiaryEntriesList';
    import { useAppDispatch } from '../../store';
    import dayjs from 'dayjs';
    
    const Diaries: FC = () => {
      const dispatch = useAppDispatch();
      const diaries = useSelector((state: RootState) => state.diaries);
      const user = useSelector((state: RootState) => state.user);
    
      useEffect(() => {
        const fetchDiaries = async () => {
          if (user) {
            http.get<null, Diary[]>(`diaries/${user.id}`).then((data) => {
              if (data && data.length > 0) {
                const sortedByUpdatedAt = data.sort((a, b) => {
                  return dayjs(b.updatedAt).unix() - dayjs(a.updatedAt).unix();
                });
                dispatch(addDiary(sortedByUpdatedAt));
              }
            });
          }
        };
        fetchDiaries();
      }, [dispatch, user]);
    
      const createDiary = async () => {
        const result = await Swal.mixin({
          input: 'text',
          confirmButtonText: 'Next →',
          showCancelButton: true,
          progressSteps: ['1', '2'],
        }).queue([
          {
            titleText: 'Diary title',
            input: 'text',
          },
          {
            titleText: 'Private or public diary?',
            input: 'radio',
            inputOptions: {
              private: 'Private',
              public: 'Public',
            },
            inputValue: 'private',
          },
        ]);
        if (result.value) {
          const { value } = result;
          const {
            diary,
            user: _user,
          } = await http.post<Partial<Diary>, { diary: Diary; user: User }>('/diaries/', {
            title: value[0],
            type: value[1],
            userId: user?.id,
          });
          if (diary && user) {
            dispatch(addDiary([diary] as Diary[]));
            dispatch(addDiary([diary] as Diary[]));
            dispatch(setUser(_user));
            return Swal.fire({
              titleText: 'All done!',
              confirmButtonText: 'OK!',
            });
          }
        }
        Swal.fire({
          titleText: 'Cancelled',
        });
      };
    
      return (
        <div style={{ padding: '1em 0.4em' }}>
          <Switch>
            <Route path="/diary/:id">
              <DiaryEntriesList />
            </Route>
            <Route path="/">
              <button onClick={createDiary}>Create New</button>
              {diaries.map((diary, idx) => (
                <DiaryTile key={idx} diary={diary} />
              ))}
            </Route>
          </Switch>
        </div>
      );
    };
    
    export default Diaries;
    

    In this component, we have a function to fetch the user’s diaries inside a useEffect hook, and a function to create a new diary. We also render our components in react-router’s <Route /> component, rendering a diary’s entries if its id matches the path param in the route /diary/:id, or otherwise rendering a list of the user’s diaries.

    To wrap things up, let’s update the Home.tsx component. First, update the imports to look like the following:

    import React, { FC } from 'react';
    import Diaries from '../diary/Diaries';
    import Editor from '../entry/Editor';
    

    Then, change the component’s return statement to the following:

    return (
      <div className="two-cols">
        <div className="left">
          <Diaries />
        </div>
        <div className="right">
          <Editor />
        </div>
      </div>
    

    Finally, replace the contents of the index.css file in your app’s src directory with the following code:

    :root {
      --primary-color: #778899;
      --error-color: #f85032;
      --text-color: #0d0d0d;
      --transition: all ease-in-out 0.3s;
    }
    body {
      margin: 0;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
        'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
        sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
    }
    html, body, #root {
      height: 100%;
    }
    *, *:before, *:after {
      box-sizing: border-box;
    }
    .auth {
      display: flex;
      align-items: center;
      height: 100%;
    }
    .card {
      background: #fff;
      padding: 3rem;
      text-align: center;
      box-shadow: 2px 8px 12px rgba(0, 0, 0, 0.1);
      max-width: 450px;
      width: 90%;
      margin: 0 auto;
    }
    .inputWrapper {
      margin: 1rem auto;
      width: 100%;
    }
    input:not([type='checkbox']), button {
      border-radius: 0.5rem;
      width: 100%;
    }
    input:not([type='checkbox']), textarea {
      border: 2px solid rgba(0, 0, 0, 0.1);
      padding: 1em;
      color: var(--text-color);
      transition: var(--transition);
    }
    input:not([type='checkbox']):focus, textarea:focus {
      outline: none;
      border-color: var(--primary-color);
    }
    button {
      appearance: none;
      border: 1px solid var(--primary-color);
      color: #fff;
      background-color: var(--primary-color);
      text-transform: uppercase;
      font-weight: bold;
      outline: none;
      cursor: pointer;
      padding: 1em;
      box-shadow: 1px 4px 6px rgba(0, 0, 0, 0.1);
      transition: var(--transition);
    }
    button.secondary {
      color: var(--primary-color);
      background-color: #fff;
      border-color: #fff;
    }
    button:hover, button:focus {
      box-shadow: 1px 6px 8px rgba(0, 0, 0, 0.1);
    }
    .error {
      margin: 0;
      margin-top: 0.2em;
      font-size: 0.8em;
      color: var(--error-color);
      animation: 0.3s ease-in-out forwards fadeIn;
    }
    .two-cols {
      display: flex;
      flex-wrap: wrap;
      height: 100vh;
    }
    .two-cols .left {
      border-right: 1px solid rgba(0, 0, 0, 0.1);
      height: 100%;
      overflow-y: scroll;
    }
    .two-cols .right {
      overflow-y: auto;
    }
    .title {
      font-size: 1.3rem;
    }
    .subtitle {
      font-size: 0.9rem;
      opacity: 0.85;
    }
    .title, .subtitle {
      margin: 0;
    }
    .diary-tile {
      border-bottom: 1px solid rgba(0, 0, 0, 0.1);
      padding: 1em;
    }
    .editor {
      height: 100%;
      padding: 1em;
    }
    .editor input {
      width: 100%;
    }
    .editor textarea {
      width: 100%;
      height: calc(100vh - 160px);
    }
    .entries ul {
      list-style: none;
      padding: 0;
    }
    .entries li {
      border-top: 1px solid rgba(0, 0, 0, 0.1);
      padding: 0.5em;
      cursor: pointer;
    }
    .entries li:nth-child(even) {
      background: rgba(0, 0, 0, 0.1);
    }
    
    @media (min-width: 768px) {
      .two-cols .left {
        width: 25%;
      }
      .two-cols .right {
        width: 75%;
      }
    }
    @keyframes fadeIn {
      0% {
        opacity: 0;
      }
      100% {
        opacity: 0.8;
      }
    }
    

    That’s it! You can now run npm start or yarn start and check out the final app at http://localhost:3000.

    Final App Home Screen (Unauthenticated User). (Large preview)

    Conclusion

    In this guide, you have learned how to rapidly develop applications using Redux. You also learned about good practices to follow when working with Redux and React, in order to make debugging and extending your applications easier. This guide is by no means extensive as there are still ongoing discussions surrounding Redux and some of its concepts. Please check out the Redux and React-Redux docs if you’d like to learn more about using Redux in your React projects.

    References

    Smashing Editorial
    (yk)

    Source link

    web design

    Setting Up Redux For Use In A Real-World Application — Smashing Magazine

    08/03/2020

    About The Author

    I love building software for the web, writing about web technologies, and playing video games.
    More about
    Jerry

    Redux is a robust state-management library for single-page Javascript apps. It is described on the official documentation as a predictable state container for Javascript applications and it’s fairly simple to learn the concepts and implement Redux in a simple app. Going from a simple counter app to a real-world app, however, can be quite the jump.

    Redux is an important library in the React ecosystem, and almost the default to use when working on React applications that involve state management. As such, the importance of knowing how it works cannot be overestimated.

    This guide will walk the reader through setting up Redux in a fairly complex React application and introduce the reader to “best practices” configuration along the way. It will be beneficial to beginners especially, and anyone who wants to fill in the gaps in their knowledge of Redux.

    Introducing Redux

    Redux is a library that aims to solve the problem of state management in JavaScript apps by imposing restrictions on how and when state updates can happen. These restrictions are formed from Redux’s “three principles” which are:

    • Single source of truth
      All of your application’s state is held in a Redux store. This state can be represented visually as a tree with a single ancestor, and the store provides methods for reading the current state and subscribing to changes from anywhere within your app.

    • State is read-only
      The only way to change the state is to send the data as a plain object, called an action. You can think about actions as a way of saying to the state, “I have some data I would like to insert/update/delete”.

    • Changes are made with pure functions
      To change your app’s state, you write a function that takes the previous state and an action and returns a new state object as the next state. This function is called a reducer, and it is a pure function because it returns the same output for a given set of inputs.

    The last principle is the most important in Redux, and this is where the magic of Redux happens. Reducer functions must not contain unpredictable code, or perform side-effects such as network requests, and should not directly mutate the state object.

    Redux is a great tool, as we’ll learn later in this guide, but it doesn’t come without its challenges or tradeoffs. To help make the process of writing Redux efficient and more enjoyable, the Redux team offers a toolkit that abstracts over the process of setting up a Redux store and provides helpful Redux add-ons and utilities that help to simplify application code. For example, the library uses Immer.js, a library that makes it possible for you to write “mutative” immutable update logic, under the hood.

    Recommended reading: Better Reducers With Immer

    In this guide, we will explore Redux by building an application that lets authenticated users create and manage digital diaries.

    Building Diaries.app

    As stated in the previous section, we will be taking a closer look at Redux by building an app that lets users create and manage diaries. We will be building our application using React, and we’ll set up Mirage as our API mocking server since we won’t have access to a real server in this guide.

    Starting a Project and Installing Dependencies

    Let’s get started on our project. First, bootstrap a new React application using create-react-app:

    Using npx:

    npx create-react-app diaries-app --template typescript
    

    We are starting with the TypeScript template, as we can improve our development experience by writing type-safe code.

    Now, let’s install the dependencies we’ll be needing. Navigate into your newly created project directory

    cd diaries-app
    

    And run the following commands:

    npm install --save redux react-redux @reduxjs/toolkit
    
    npm install --save axios react-router-dom react-hook-form yup dayjs markdown-to-jsx sweetalert2
    
    npm install --save-dev miragejs @types/react-redux @types/react-router-dom @types/yup @types/markdown-to-jsx
    

    The first command will install Redux, React-Redux (official React bindings for Redux), and the Redux toolkit.

    The second command installs some extra packages which will be useful for the app we’ll be building but are not required to work with Redux.

    The last command installs Mirage and type declarations for the packages we installed as devDependencies.

    Describing the Application’s Initial State

    Let’s go over our application’s requirements in detail. The application will allow authenticated users to create or modify existing diaries. Diaries are private by default, but they can be made public. Finally, diary entries will be sorted by their last modified date.

    This relationship should look something like this:

    An Overview of the Application’s Data Model. (Large preview)

    Armed with this information, we can now model our application’s state. First, we will create an interface for each of the following resources: User, Diary and DiaryEntry. Interfaces in Typescript describe the shape of an object.

    Go ahead and create a new directory named interfaces in your app’s src sub-directory:

    cd src && mkdir interfaces
    

    Next, run the following commands in the directory you just created:

    touch entry.interface.ts
    touch diary.interface.ts
    touch user.interface.ts
    

    This will create three files named entry.interface.ts, diary.interface.ts and user.interface.ts respectively. I prefer to keep interfaces that would be used in multiple places across my app in a single location.

    Open entry.interface.ts and add the following code to set up the Entry interface:

    export interface Entry {
      id?: string;
      title: string;
      content: string;
      createdAt?: string;
      updatedAt?: string;
      diaryId?: string;
    }
    

    A typical diary entry will have a title and some content, as well as information about when it was created or last updated. We’ll get back to the diaryId property later.

    Next, add the following to diary.interface.ts:

    export interface Diary {
      id?: string;
      title: string;
      type: 'private' | 'public';
      createdAt?: string;
      updatedAt?: string;
      userId?: string;
      entryIds: string[] | null;
    }
    

    Here, we have a type property which expects an exact value of either ‘private’ or ‘public’, as diaries must be either private or public. Any other value will throw an error in the TypeScript compiler.

    We can now describe our User object in the user.interface.ts file as follows:

    export interface User {
      id?: string;
      username: string;
      email: string;
      password?: string;
      diaryIds: string[] | null;
    }
    

    With our type definitions finished and ready to be used across our app, let’s setup our mock API server using Mirage.

    Setting up API Mocking with MirageJS

    Since this tutorial is focused on Redux, we will not go into the details of setting up and using Mirage in this section. Please check out this excellent series if you would like to learn more about Mirage.

    To get started, navigate to your src directory and create a file named server.ts by running the following commands:

    mkdir -p services/mirage
    cd services/mirage
    
    # ~/diaries-app/src/services/mirage
    touch server.ts
    

    Next, open the server.ts file and add the following code:

    import { Server, Model, Factory, belongsTo, hasMany, Response } from 'miragejs';
    
    export const handleErrors = (error: any, message = 'An error ocurred') => {
      return new Response(400, undefined, {
        data: {
          message,
          isError: true,
        },
      });
    };
    
    export const setupServer = (env?: string): Server => {
      return new Server({
        environment: env ?? 'development',
    
        models: {
          entry: Model.extend({
            diary: belongsTo(),
          }),
          diary: Model.extend({
            entry: hasMany(),
            user: belongsTo(),
          }),
          user: Model.extend({
            diary: hasMany(),
          }),
        },
    
        factories: {
          user: Factory.extend({
            username: 'test',
            password: 'password',
            email: 'test@email.com',
          }),
        },
    
        seeds: (server): any => {
          server.create('user');
        },
    
        routes(): void {
          this.urlPrefix = 'https://diaries.app';
        },
      });
    };
    

    In this file, we are exporting two functions. A utility function for handling errors, and setupServer(), which returns a new server instance. The setupServer() function takes an optional argument which can be used to change the server’s environment. You can use this to set up Mirage for testing later.

    We have also defined three models in the server’s models property: User, Diary and Entry. Remember that earlier we set up the Entry interface with a property named diaryId. This value will be automatically set to the id the entry is being saved to. Mirage uses this property to establish a relationship between an Entry and a Diary. The same thing also happens when a user creates a new diary: userId is automatically set to that user’s id.

    We seeded the database with a default user and configured Mirage to intercept all requests from our app starting with https://diaries.app. Notice that we haven’t configured any route handlers yet. Let’s go ahead and create a few.

    Ensure that you are in the src/services/mirage directory, then create a new directory named routes using the following command:

    # ~/diaries-app/src/services/mirage
    mkdir routes
    

    cd to the newly created directory and create a file named user.ts:

    cd routes
    touch user.ts
    

    Next, paste the following code in the user.ts file:

    import { Response, Request } from 'miragejs';
    import { handleErrors } from '../server';
    import { User } from '../../../interfaces/user.interface';
    import { randomBytes } from 'crypto';
    
    const generateToken = () => randomBytes(8).toString('hex');
    
    export interface AuthResponse {
      token: string;
      user: User;
    }
    
    const login = (schema: any, req: Request): AuthResponse | Response => {
      const { username, password } = JSON.parse(req.requestBody);
      const user = schema.users.findBy({ username });
      if (!user) {
        return handleErrors(null, 'No user with that username exists');
      }
      if (password !== user.password) {
        return handleErrors(null, 'Password is incorrect');
      }
      const token = generateToken();
      return {
        user: user.attrs as User,
        token,
      };
    };
    
    const signup = (schema: any, req: Request): AuthResponse | Response => {
      const data = JSON.parse(req.requestBody);
      const exUser = schema.users.findBy({ username: data.username });
      if (exUser) {
        return handleErrors(null, 'A user with that username already exists.');
      }
      const user = schema.users.create(data);
      const token = generateToken();
      return {
        user: user.attrs as User,
        token,
      };
    };
    
    export default {
      login,
      signup,
    };
    

    The login and signup methods here receive a Schema class and a fake Request object and, upon validating the password or checking that the login does not already exist, return the existing user or a new user respectively. We use the Schema object to interact with Mirage’s ORM, while the Request object contains information about the intercepted request including the request body and headers.

    Next, let’s add methods for working with diaries and diary entries. Create a file named diary.ts in your routes directory:

    touch diary.ts
    

    Update the file with the following methods for working with Diary resources:

    export const create = (
      schema: any,
      req: Request
    ): { user: User; diary: Diary } | Response => {
      try {
        const { title, type, userId } = JSON.parse(req.requestBody) as Partial<
          Diary
        >;
        const exUser = schema.users.findBy({ id: userId });
        if (!exUser) {
          return handleErrors(null, 'No such user exists.');
        }
        const now = dayjs().format();
        const diary = exUser.createDiary({
          title,
          type,
          createdAt: now,
          updatedAt: now,
        });
        return {
          user: {
            ...exUser.attrs,
          },
          diary: diary.attrs,
        };
      } catch (error) {
        return handleErrors(error, 'Failed to create Diary.');
      }
    };
    
    export const updateDiary = (schema: any, req: Request): Diary | Response => {
      try {
        const diary = schema.diaries.find(req.params.id);
        const data = JSON.parse(req.requestBody) as Partial<Diary>;
        const now = dayjs().format();
        diary.update({
          ...data,
          updatedAt: now,
        });
        return diary.attrs as Diary;
      } catch (error) {
        return handleErrors(error, 'Failed to update Diary.');
      }
    };
    
    export const getDiaries = (schema: any, req: Request): Diary[] | Response => {
      try {
        const user = schema.users.find(req.params.id);
        return user.diary as Diary[];
      } catch (error) {
        return handleErrors(error, 'Could not get user diaries.');
      }
    };
    

    Next, let’s add some methods for working with diary entries:

    export const addEntry = (
      schema: any,
      req: Request
    ): { diary: Diary; entry: Entry } | Response => {
      try {
        const diary = schema.diaries.find(req.params.id);
        const { title, content } = JSON.parse(req.requestBody) as Partial<Entry>;
        const now = dayjs().format();
        const entry = diary.createEntry({
          title,
          content,
          createdAt: now,
          updatedAt: now,
        });
        diary.update({
          ...diary.attrs,
          updatedAt: now,
        });
        return {
          diary: diary.attrs,
          entry: entry.attrs,
        };
      } catch (error) {
        return handleErrors(error, 'Failed to save entry.');
      }
    };
    
    export const getEntries = (
      schema: any,
      req: Request
    ): { entries: Entry[] } | Response => {
      try {
        const diary = schema.diaries.find(req.params.id);
        return diary.entry;
      } catch (error) {
        return handleErrors(error, 'Failed to get Diary entries.');
      }
    };
    
    export const updateEntry = (schema: any, req: Request): Entry | Response => {
      try {
        const entry = schema.entries.find(req.params.id);
        const data = JSON.parse(req.requestBody) as Partial<Entry>;
        const now = dayjs().format();
        entry.update({
          ...data,
          updatedAt: now,
        });
        return entry.attrs as Entry;
      } catch (error) {
        return handleErrors(error, 'Failed to update entry.');
      }
    };
    

    Finally, let’s add the necessary imports at the top of the file:

    import { Response, Request } from 'miragejs';
    import { handleErrors } from '../server';
    import { Diary } from '../../../interfaces/diary.interface';
    import { Entry } from '../../../interfaces/entry.interface';
    import dayjs from 'dayjs';
    import { User } from '../../../interfaces/user.interface';
    

    In this file, we have exported methods for working with the Diary and Entry models. In the create method, we call a method named user.createDiary() to save a new diary and associate it to a user account.

    The addEntry and updateEntry methods create and correctly associate a new entry to a diary or update an existing entry’s data respectively. The latter also updates the entry’s updatedAt property with the current timestamp. The updateDiary method also updates a diary with the timestamp the change was made. Later, we’ll be sorting the records we receive from our network request with this property.

    We also have a getDiaries method which retrieves a user’s diaries and a getEntries methods which retrieves a selected diary’s entries.

    We can now update our server to use the methods we just created. Open server.ts to include the files:

    import { Server, Model, Factory, belongsTo, hasMany, Response } from 'miragejs';
    
    import user from './routes/user';
    import * as diary from './routes/diary';
    

    Then, update the server’s route property with the routes we want to handle:

    export const setupServer = (env?: string): Server => {
      return new Server({
        // ...
        routes(): void {
          this.urlPrefix = 'https://diaries.app';
    
          this.get('/diaries/entries/:id', diary.getEntries);
          this.get('/diaries/:id', diary.getDiaries);
    
          this.post('/auth/login', user.login);
          this.post('/auth/signup', user.signup);
    
          this.post('/diaries/', diary.create);
          this.post('/diaries/entry/:id', diary.addEntry);
    
          this.put('/diaries/entry/:id', diary.updateEntry);
          this.put('/diaries/:id', diary.updateDiary);
        },
      });
    };
    

    With this change, when a network request from our app matches one of the route handlers, Mirage intercepts the request and invokes the respective route handler functions.

    Next, we’ll proceed to make our application aware of the server. Open src/index.tsx and import the setupServer() method:

    import { setupServer } from './services/mirage/server';
    

    And add the following code before ReactDOM.render():

    if (process.env.NODE_ENV === 'development') {
      setupServer();
    }
    

    The check in the code block above ensures that our Mirage server will run only while we are in development mode.

    One last thing we need to do before moving on to the Redux bits is configure a custom Axios instance for use in our app. This will help to reduce the amount of code we’ll have to write later on.

    Create a file named api.ts under src/services and add the following code to it:

    import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios';
    import { showAlert } from '../util';
    
    const http: AxiosInstance = axios.create({
      baseURL: 'https://diaries.app',
    });
    
    http.defaults.headers.post['Content-Type'] = 'application/json';
    
    http.interceptors.response.use(
      async (response: AxiosResponse): Promise => {
        if (response.status >= 200 && response.status < 300) {
          return response.data;
        }
      },
      (error: AxiosError) => {
        const { response, request }: {
          response?: AxiosResponse;
          request?: XMLHttpRequest;
        } = error;
        if (response) {
          if (response.status >= 400 && response.status < 500) {
            showAlert(response.data?.data?.message, 'error');
            return null;
          }
        } else if (request) {
          showAlert('Request failed. Please try again.', 'error');
          return null;
        }
        return Promise.reject(error);
      }
    );
    
    export default http;
    

    In this file, we are exporting an Axios instance modified to include our app’s API url, https://diaries.app. We have configured an interceptor to handle success and error responses, and we display error messages using a sweetalert toast which we will configure in the next step.

    Create a file named util.ts in your src directory and paste the following code in it:

    import Swal, { SweetAlertIcon } from 'sweetalert2';
    
    export const showAlert = (titleText = 'Something happened.', alertType?: SweetAlertIcon): void => {
      Swal.fire({
        titleText,
        position: 'top-end',
        timer: 3000,
        timerProgressBar: true,
        toast: true,
        showConfirmButton: false,
        showCancelButton: true,
        cancelButtonText: 'Dismiss',
        icon: alertType,
        showClass: {
          popup: 'swal2-noanimation',
          backdrop: 'swal2-noanimation',
        },
        hideClass: {
          popup: '',
          backdrop: '',
        },
      });
    };
    

    This file exports a function that displays a toast whenever it is invoked. The function accepts parameters to allow you set the toast message and type. For example, we are showing an error toast in the Axios response error interceptor like this:

    showAlert(response.data?.data?.message, 'error');
    

    Now when we make requests from our app while in development mode, they will be intercepted and handled by Mirage instead. In the next section, we will set up our Redux store using Redux toolkit.

    Setting up a Redux Store

    In this section, we are going to set up our store using the following exports from Redux toolkit: configureStore(), getDefaultMiddleware() and createSlice(). Before we start, we should take a detailed look at what these exports do.

    configureStore() is an abstraction over the Redux createStore() function that helps simplify your code. It uses createStore() internally to set up your store with some useful development tools:

    export const store = configureStore({
      reducer: rootReducer, // a single reducer function or an object of slice reducers
    });
    

    The createSlice() function helps simplify the process of creating action creators and slice reducers. It accepts an initial state, an object full of reducer functions, and a “slice name”, and automatically generates action creators and action types corresponding to the reducers and your state. It also returns a single reducer function, which can be passed to Redux’s combineReducers() function as a “slice reducer”.

    Remember that the state is a single tree, and a single root reducer manages changes to that tree. For maintainability, it is recommended to split your root reducer into “slices,” and have a “slice reducer” provide an initial value and calculate the updates to a corresponding slice of the state. These slices can be joined into a single reducer function by using combineReducers().

    There are additional options for configuring the store. For example, you can pass an array of your own middleware to configureStore() or start up your app from a saved state using the preloadedState option. When you supply the middleware option, you have to define all the middleware you want added to the store. If you would like to retain the defaults when setting up your store, you can use getDefaultMiddleware() to get the default list of middleware:

    export const store = configureStore({
      // ...
      middleware: [...getDefaultMiddleware(), customMiddleware],
    });
    

    Let’s now proceed to set up our store. We will adopt a “ducks-style” approach to structuring our files, specifically following the guidelines in practice from the Github Issues sample app. We will be organizing our code such that related components, as well as actions and reducers, live in the same directory. The final state object will look like this:

    type RootState = {
      auth: {
        token: string | null;
        isAuthenticated: boolean;
      };
      diaries: Diary[];
      entries: Entry[];
      user: User | null;
      editor: {
        canEdit: boolean;
        currentlyEditing: Entry | null;
        activeDiaryId: string | null;
      };
    }
    

    To get started, create a new directory named features under your src directory:

    # ~/diaries-app/src
    mkdir features
    

    Then, cd into features and create directories named auth, diary and entry:

    cd features
    mkdir auth diary entry
    

    cd into the auth directory and create a file named authSlice.ts:

    cd auth
    # ~/diaries-app/src/features/auth
    touch authSlice.ts
    

    Open the file and paste the following in it:

    import { createSlice, PayloadAction } from '@reduxjs/toolkit';
    
    interface AuthState {
      token: string | null;
      isAuthenticated: boolean;
    }
    
    const initialState: AuthState = {
      token: null,
      isAuthenticated: false,
    };
    
    const auth = createSlice({
      name: 'auth',
      initialState,
      reducers: {
        saveToken(state, { payload }: PayloadAction) {
          if (payload) {
            state.token = payload;
          }
        },
        clearToken(state) {
          state.token = null;
        },
        setAuthState(state, { payload }: PayloadAction) {
          state.isAuthenticated = payload;
        },
      },
    });
    
    export const { saveToken, clearToken, setAuthState } = auth.actions;
    export default auth.reducer;
    

    In this file, we’re creating a slice for the auth property of our app’s state using the createSlice() function introduced earlier. The reducers property holds a map of reducer functions for updating values in the auth slice. The returned object contains automatically generated action creators and a single slice reducer. We would need to use these in other files so, following the “ducks pattern”, we do named exports of the action creators, and a default export of the reducer function.

    Let’s set up the remaining reducer slices according to the app state we saw earlier. First, create a file named userSlice.ts in the auth directory and add the following code to it:

    import { createSlice, PayloadAction } from '@reduxjs/toolkit';
    import { User } from '../../interfaces/user.interface';
    
    const user = createSlice({
      name: 'user',
      initialState: null as User | null,
      reducers: {
        setUser(state, { payload }: PayloadAction<User | null>) {
          return state = (payload != null) ? payload : null;
        },
      },
    });
    
    export const { setUser } = user.actions;
    export default user.reducer;
    

    This creates a slice reducer for the user property in our the application’s store. The setUser reducer function accepts a payload containing user data and updates the state with it. When no data is passed, we set the state’s user property to null.

    Next, create a file named diariesSlice.ts under src/features/diary:

    # ~/diaries-app/src/features
    cd diary
    touch diariesSlice.ts
    

    Add the following code to the file:

    import { createSlice, PayloadAction } from '@reduxjs/toolkit';
    import { Diary } from '../../interfaces/diary.interface';
    
    const diaries = createSlice({
      name: 'diaries',
      initialState: [] as Diary[],
      reducers: {
        addDiary(state, { payload }: PayloadAction<Diary[]>) {
          const diariesToSave = payload.filter((diary) => {
            return state.findIndex((item) => item.id === diary.id) === -1;
          });
          state.push(...diariesToSave);
        },
        updateDiary(state, { payload }: PayloadAction<Diary>) {
          const { id } = payload;
          const diaryIndex = state.findIndex((diary) => diary.id === id);
          if (diaryIndex !== -1) {
            state.splice(diaryIndex, 1, payload);
          }
        },
      },
    });
    
    export const { addDiary, updateDiary } = diaries.actions;
    export default diaries.reducer;
    

    The “diaries” property of our state is an array containing the user’s diaries, so our reducer functions here all work on the state object they receive using array methods. Notice here that we are writing normal “mutative” code when working on the state. This is possible because the reducer functions we create using the createSlice() method are wrapped with Immer’s produce() method. This results in Immer returning a correct immutably updated result for our state regardless of us writing mutative code.

    Next, create a file named entriesSlice.ts under src/features/entry:

    # ~/diaries-app/src/features
    mkdir entry
    cd entry
    touch entriesSlice.ts
    

    Open the file and add the following code:

    import { createSlice, PayloadAction } from '@reduxjs/toolkit';
    import { Entry } from '../../interfaces/entry.interface';
    
    const entries = createSlice({
      name: 'entries',
      initialState: [] as Entry[],
      reducers: {
        setEntries(state, { payload }: PayloadAction<Entry[] | null>) {
          return (state = payload != null ? payload : []);
        },
        updateEntry(state, { payload }: PayloadAction<Entry>) {
          const { id } = payload;
          const index = state.findIndex((e) => e.id === id);
          if (index !== -1) {
            state.splice(index, 1, payload);
          }
        },
      },
    });
    
    export const { setEntries, updateEntry } = entries.actions;
    export default entries.reducer;
    

    The reducer functions here have logic similar to the previous slice’s reducer functions. The entries property is also an array, but it only holds entries for a single diary. In our app, this will be the diary currently in the user’s focus.

    Finally, create a file named editorSlice.ts in src/features/entry and add the following to it:

    import { createSlice, PayloadAction } from '@reduxjs/toolkit';
    import { Entry } from '../../interfaces/entry.interface';
    
    interface EditorState {
      canEdit: boolean;
      currentlyEditing: Entry | null;
      activeDiaryId: string | null;
    }
    
    const initialState: EditorState = {
      canEdit: false,
      currentlyEditing: null,
      activeDiaryId: null,
    };
    
    const editor = createSlice({
      name: 'editor',
      initialState,
      reducers: {
        setCanEdit(state, { payload }: PayloadAction<boolean>) {
          state.canEdit = payload != null ? payload : !state.canEdit;
        },
        setCurrentlyEditing(state, { payload }: PayloadAction<Entry | null>) {
          state.currentlyEditing = payload;
        },
        setActiveDiaryId(state, { payload }: PayloadAction<string>) {
          state.activeDiaryId = payload;
        },
      },
    });
    
    export const { setCanEdit, setCurrentlyEditing, setActiveDiaryId } = editor.actions;
    export default editor.reducer;
    

    Here, we have a slice for the editor property in state. We’ll be using the properties in this object to check if the user wants to switch to editing mode, which diary the edited entry belongs to, and what entry is going to be edited.

    To put it all together, create a file named rootReducer.ts in the src directory with the following content:

    import { combineReducers } from '@reduxjs/toolkit';
    import authReducer from './features/auth/authSlice';
    import userReducer from './features/auth/userSlice';
    import diariesReducer from './features/diary/diariesSlice';
    import entriesReducer from './features/entry/entriesSlice';
    import editorReducer from './features/entry/editorSlice';
    
    const rootReducer = combineReducers({
      auth: authReducer,
      diaries: diariesReducer,
      entries: entriesReducer,
      user: userReducer,
      editor: editorReducer,
    });
    
    export type RootState = ReturnType<typeof rootReducer>;
    export default rootReducer;
    

    In this file, we’ve combined our slice reducers into a single root reducer with the combineReducers() function. We’ve also exported the RootState type, which will be useful later when we’re selecting values from the store. We can now use the root reducer (the default export of this file) to set up our store.

    Create a file named store.ts with the following contents:

    import { configureStore } from '@reduxjs/toolkit';
    import rootReducer from './rootReducer';
    import { useDispatch } from 'react-redux';
    
    const store = configureStore({
      reducer: rootReducer,
    });
    
    type AppDispatch = typeof store.dispatch;
    export const useAppDispatch = () => useDispatch<AppDispatch>();
    export default store;
    

    With this, we’ve created a store using the configureStore() export from Redux toolkit. We’ve also exported an hook called useAppDispatch() which merely returns a typed useDispatch() hook.

    Next, update the imports in index.tsx to look like the following:

    import React from 'react';
    import ReactDOM from 'react-dom';
    import './index.css';
    import App from './app/App';
    import * as serviceWorker from './serviceWorker';
    import { setupServer } from './services/mirage/server';
    import { Provider } from 'react-redux';
    import store from './store';
    // ...
    

    Finally, make the store available to the app’s components by wrapping <App /> (the top-level component) with <Provider />:

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

    Now, if you start your app and you navigate to http://localhost:3000 with the Redux Dev Tools extension enabled, you should see the following in your app’s state:

    Initial State in Redux Dev Tools Extension. (Large preview)

    Great work so far, but we’re not quite finished yet. In the next section, we will design the app’s User Interface and add functionality using the store we’ve just created.

    Designing The Application User Interface

    To see Redux in action, we are going to build a demo app. In this section, we will connect our components to the store we’ve created and learn to dispatch actions and modify the state using reducer functions. We will also learn how to read values from the store. Here’s what our Redux-powered application will look like.

    Home page showing an authenticated user’s diaries. (Large preview)
    Screenshots of final app. (Large preview)

    Setting up the Authentication Feature

    To get started, move App.tsx and its related files from the src directory to its own directory like this:

    # ~/diaries-app/src
    mkdir app
    mv App.tsx App.test.tsx app
    

    You can delete the App.css and logo.svg files as we won’t be needing them.

    Next, open the App.tsx file and replace its contents with the following:

    import React, { FC, lazy, Suspense } from 'react';
    import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
    import { useSelector } from 'react-redux';
    import { RootState } from '../rootReducer';
    
    const Auth = lazy(() => import('../features/auth/Auth'));
    const Home = lazy(() => import('../features/home/Home'));
    
    const App: FC = () => {
      const isLoggedIn = useSelector(
        (state: RootState) => state.auth.isAuthenticated
      );
      return (
        <Router>
          <Switch>
            <Route path="/">
              <Suspense fallback={<p>Loading...</p>}>
                {isLoggedIn ? <Home /> : <Auth />}
              </Suspense>
            </Route>
          </Switch>
        </Router>
      );
    };
    
    export default App;
    

    Here we have set up our app to render an <Auth /> component if the user is unauthenticated, or otherwise render a <Home /> component. We haven’t created either of these components yet, so let’s fix that. Create a file named Auth.tsx under src/features/auth and add the following contents to the file:

    import React, { FC, useState } from 'react';
    import { useForm } from 'react-hook-form';
    import { User } from '../../interfaces/user.interface';
    import * as Yup from 'yup';
    import http from '../../services/api';
    import { saveToken, setAuthState } from './authSlice';
    import { setUser } from './userSlice';
    import { AuthResponse } from '../../services/mirage/routes/user';
    import { useAppDispatch } from '../../store';
    
    const schema = Yup.object().shape({
      username: Yup.string()
        .required('What? No username?')
        .max(16, 'Username cannot be longer than 16 characters'),
      password: Yup.string().required('Without a password, "None shall pass!"'),
      email: Yup.string().email('Please provide a valid email address (abc@xy.z)'),
    });
    
    const Auth: FC = () => {
      const { handleSubmit, register, errors } = useForm<User>({
        validationSchema: schema,
      });
      const [isLogin, setIsLogin] = useState(true);
      const [loading, setLoading] = useState(false);
      const dispatch = useAppDispatch();
    
      const submitForm = (data: User) => {
        const path = isLogin ? '/auth/login' : '/auth/signup';
        http
          .post<User, AuthResponse>(path, data)
          .then((res) => {
            if (res) {
              const { user, token } = res;
              dispatch(saveToken(token));
              dispatch(setUser(user));
              dispatch(setAuthState(true));
            }
          })
          .catch((error) => {
            console.log(error);
          })
          .finally(() => {
            setLoading(false);
          });
      };
    
      return (
        <div className="auth">
          <div className="card">
            <form onSubmit={handleSubmit(submitForm)}>
              <div className="inputWrapper">
                <input ref={register} name="username" placeholder="Username" />
                {errors && errors.username && (
                  <p className="error">{errors.username.message}</p>
                )}
              </div>
              <div className="inputWrapper">
                <input
                  ref={register}
                  name="password"
                  type="password"
                  placeholder="Password"
                />
                {errors && errors.password && (
                  <p className="error">{errors.password.message}</p>
                )}
              </div>
              {!isLogin && (
                <div className="inputWrapper">
                  <input
                    ref={register}
                    name="email"
                    placeholder="Email (optional)"
                  />
                  {errors && errors.email && (
                    <p className="error">{errors.email.message}</p>
                  )}
                </div>
              )}
              <div className="inputWrapper">
                <button type="submit" disabled={loading}>
                  {isLogin ? 'Login' : 'Create account'}
                </button>
              </div>
              <p
                onClick={() => setIsLogin(!isLogin)}
                style={{ cursor: 'pointer', opacity: 0.7 }}
              >
                {isLogin ? 'No account? Create one' : 'Already have an account?'}
              </p>
            </form>
          </div>
        </div>
      );
    };
    
    export default Auth;
    

    In this component, we have set up a form for users to log in, or to create an account. Our form fields are validated using Yup and, on successfully authenticating a user, we use our useAppDispatch hook to dispatch the relevant actions. You can see the dispatched actions and the changes made to your state in the Redux DevTools Extension:

    Dispatched Actions with Changes Tracked in Redux Dev Tools Extensions. (Large preview)

    Finally, create a file named Home.tsx under src/features/home and add the following code to the file:

    import React, { FC } from 'react';
    
    const Home: FC = () => {
      return (
        <div>
          <p>Welcome user!</p>
        </div>
      );
    };
    
    export default Home;
    

    For now, we are just displaying some text to the authenticated user. As we build the rest of our application, we will be updating this file.

    Setting up the Editor

    The next component we are going to build is the editor. Though basic, we will enable support for rendering markdown content using the markdown-to-jsx library we installed earlier.

    First, create a file named Editor.tsx in the src/features/entry directory. Then, add the following code to the file:

    import React, { FC, useState, useEffect } from 'react';
    import { useSelector } from 'react-redux';
    import { RootState } from '../../rootReducer';
    import Markdown from 'markdown-to-jsx';
    import http from '../../services/api';
    import { Entry } from '../../interfaces/entry.interface';
    import { Diary } from '../../interfaces/diary.interface';
    import { setCurrentlyEditing, setCanEdit } from './editorSlice';
    import { updateDiary } from '../diary/diariesSlice';
    import { updateEntry } from './entriesSlice';
    import { showAlert } from '../../util';
    import { useAppDispatch } from '../../store';
    
    const Editor: FC = () => {
      const { currentlyEditing: entry, canEdit, activeDiaryId } = useSelector(
        (state: RootState) => state.editor
      );
      const [editedEntry, updateEditedEntry] = useState(entry);
      const dispatch = useAppDispatch();
    
      const saveEntry = async () => {
        if (activeDiaryId == null) {
          return showAlert('Please select a diary.', 'warning');
        }
        if (entry == null) {
          http
            .post<Entry, { diary: Diary; entry: Entry }>(
              `/diaries/entry/${activeDiaryId}`,
              editedEntry
            )
            .then((data) => {
              if (data != null) {
                const { diary, entry: _entry } = data;
                dispatch(setCurrentlyEditing(_entry));
                dispatch(updateDiary(diary));
              }
            });
        } else {
          http
            .put<Entry, Entry>(`diaries/entry/${entry.id}`, editedEntry)
            .then((_entry) => {
              if (_entry != null) {
                dispatch(setCurrentlyEditing(_entry));
                dispatch(updateEntry(_entry));
              }
            });
        }
        dispatch(setCanEdit(false));
      };
    
      useEffect(() => {
        updateEditedEntry(entry);
      }, [entry]);
    
      return (
        <div className="editor">
          <header
            style={{
              display: 'flex',
              flexWrap: 'wrap',
              alignItems: 'center',
              marginBottom: '0.2em',
              paddingBottom: '0.2em',
              borderBottom: '1px solid rgba(0,0,0,0.1)',
            }}
          >
            {entry && !canEdit ? (
              <h4>
                {entry.title}
                <a
                  href="#edit"
                  onClick={(e) => {
                    e.preventDefault();
                    if (entry != null) {
                      dispatch(setCanEdit(true));
                    }
                  }}
                  style={{ marginLeft: '0.4em' }}
                >
                  (Edit)
                </a>
              </h4>
            ) : (
              <input
                value={editedEntry?.title ?? ''}
                disabled={!canEdit}
                onChange={(e) => {
                  if (editedEntry) {
                    updateEditedEntry({
                      ...editedEntry,
                      title: e.target.value,
                    });
                  } else {
                    updateEditedEntry({
                      title: e.target.value,
                      content: '',
                    });
                  }
                }}
              />
            )}
          </header>
          {entry && !canEdit ? (
            <Markdown>{entry.content}</Markdown>
          ) : (
            <>
              <textarea
                disabled={!canEdit}
                placeholder="Supports markdown!"
                value={editedEntry?.content ?? ''}
                onChange={(e) => {
                  if (editedEntry) {
                    updateEditedEntry({
                      ...editedEntry,
                      content: e.target.value,
                    });
                  } else {
                    updateEditedEntry({
                      title: '',
                      content: e.target.value,
                    });
                  }
                }}
              />
              <button onClick={saveEntry} disabled={!canEdit}>
                Save
              </button>
            </>
          )}
        </div>
      );
    };
    
    export default Editor;
    

    Let’s break down what’s happening in the Editor component.

    First, we are picking some values (with correctly inferred types) from the app’s state using the useSelector() hook from react-redux. In the next line, we have a stateful value called editedEntry whose initial value is set to the editor.currentlyEditing property we’ve selected from the store.

    Next, we have the saveEntry function which updates or creates a new entry in the API, and dispatches the respective Redux action.

    Finally, we have a useEffect that is fired when the editor.currentlyEditing property changes. Our editor’s UI (in the component’s return function) has been set up to respond to changes in the state. For example, rendering the entry’s content as JSX elements when the user isn’t editing.

    With that, the app’s Entry feature should be completely set up. In the next section, we will finish building the Diary feature and then import the main components in the Home component we created earlier.

    Final Steps

    To finish up our app, we will first create components for the Diary feature. Then, we will update the Home component with the primary exports from the Diary and Entry features. Finally, we will add some styling to give our app the required pizzazz!

    Let’s start by creating a file in src/features/diary named DiaryTile.tsx. This component will present information about a diary and its entries, and allow the user to edit the diary’s title. Add the following code to the file:

    import React, { FC, useState } from 'react';
    import { Diary } from '../../interfaces/diary.interface';
    import http from '../../services/api';
    import { updateDiary } from './diariesSlice';
    import { setCanEdit, setActiveDiaryId, setCurrentlyEditing } from '../entry/editorSlice';
    import { showAlert } from '../../util';
    import { Link } from 'react-router-dom';
    import { useAppDispatch } from '../../store';
    
    interface Props {
      diary: Diary;
    }
    
    const buttonStyle: React.CSSProperties = {
      fontSize: '0.7em',
      margin: '0 0.5em',
    };
    
    const DiaryTile: FC<Props> = (props) => {
      const [diary, setDiary] = useState(props.diary);
      const [isEditing, setIsEditing] = useState(false);
      const dispatch = useAppDispatch();
      const totalEntries = props.diary?.entryIds?.length;
    
      const saveChanges = () => {
        http
          .put<Diary, Diary>(`/diaries/${diary.id}`, diary)
          .then((diary) => {
            if (diary) {
              dispatch(updateDiary(diary));
              showAlert('Saved!', 'success');
            }
          })
          .finally(() => {
            setIsEditing(false);
          });
      };
    
      return (
        <div className="diary-tile">
          <h2
            className="title"
            title="Click to edit"
            onClick={() => setIsEditing(true)}
            style={{
              cursor: 'pointer',
            }}
          >
            {isEditing ? (
              <input
                value={diary.title}
                onChange={(e) => {
                  setDiary({
                    ...diary,
                    title: e.target.value,
                  });
                }}
                onKeyUp={(e) => {
                  if (e.key === 'Enter') {
                    saveChanges();
                  }
                }}
              />
            ) : (
              <span>{diary.title}</span>
            )}
          </h2>
          <p className="subtitle">{totalEntries ?? '0'} saved entries</p>
          <div style={{ display: 'flex' }}>
            <button
              style={buttonStyle}
              onClick={() => {
                dispatch(setCanEdit(true));
                dispatch(setActiveDiaryId(diary.id as string));
                dispatch(setCurrentlyEditing(null));
              }}
            >
              Add New Entry
            </button>
            <Link to={`diary/${diary.id}`} style={{ width: '100%' }}>
              <button className="secondary" style={buttonStyle}>
                View all →
              </button>
            </Link>
          </div>
        </div>
      );
    };
    
    export default DiaryTile;
    

    In this file, we receive a diary object as a prop and display the data in our component. Notice that we use local state and component props for our data display here. That’s because you don’t have to manage all your app’s state using Redux. Sharing data using props, and maintaining local state in your components is acceptable and encouraged in some cases.

    Next, let’s create a component that will display a list of a diary’s entries, with the last updated entries at the top of the list. Ensure you are in the src/features/diary directory, then create a file named DiaryEntriesList.tsx and add the following code to the file:

    import React, { FC, useEffect } from 'react';
    import { useParams, Link } from 'react-router-dom';
    import { useSelector } from 'react-redux';
    import { RootState } from '../../rootReducer';
    import http from '../../services/api';
    import { Entry } from '../../interfaces/entry.interface';
    import { setEntries } from '../entry/entriesSlice';
    import { setCurrentlyEditing, setCanEdit } from '../entry/editorSlice';
    import dayjs from 'dayjs';
    import { useAppDispatch } from '../../store';
    
    const DiaryEntriesList: FC = () => {
      const { entries } = useSelector((state: RootState) => state);
      const dispatch = useAppDispatch();
      const { id } = useParams();
    
      useEffect(() => {
        if (id != null) {
          http
            .get<null, { entries: Entry[] }>(`/diaries/entries/${id}`)
            .then(({ entries: _entries }) => {
              if (_entries) {
                const sortByLastUpdated = _entries.sort((a, b) => {
                  return dayjs(b.updatedAt).unix() - dayjs(a.updatedAt).unix();
                });
                dispatch(setEntries(sortByLastUpdated));
              }
            });
        }
      }, [id, dispatch]);
    
      return (
        <div className="entries">
          <header>
            <Link to="/">
              <h3>← Go Back</h3>
            </Link>
          </header>
          <ul>
            {entries.map((entry) => (
              <li
                key={entry.id}
                onClick={() => {
                  dispatch(setCurrentlyEditing(entry));
                  dispatch(setCanEdit(true));
                }}
              >
                {entry.title}
              </li>
            ))}
          </ul>
        </div>
      );
    };
    
    export default DiaryEntriesList;
    

    Here, we subscribe to the entries property of our app’s state, and have our effect fetch a diary’s entry only run when a property, id, changes. This property’s value is gotten from our URL as a path parameter using the useParams() hook from react-router. In the next step, we will create a component that will enable users to create and view diaries, as well as render a diary’s entries when it is in focus.

    Create a file named Diaries.tsx while still in the same directory, and add the following code to the file:

    import React, { FC, useEffect } from 'react';
    import { useSelector } from 'react-redux';
    import { RootState } from '../../rootReducer';
    import http from '../../services/api';
    import { Diary } from '../../interfaces/diary.interface';
    import { addDiary } from './diariesSlice';
    import Swal from 'sweetalert2';
    import { setUser } from '../auth/userSlice';
    import DiaryTile from './DiaryTile';
    import { User } from '../../interfaces/user.interface';
    import { Route, Switch } from 'react-router-dom';
    import DiaryEntriesList from './DiaryEntriesList';
    import { useAppDispatch } from '../../store';
    import dayjs from 'dayjs';
    
    const Diaries: FC = () => {
      const dispatch = useAppDispatch();
      const diaries = useSelector((state: RootState) => state.diaries);
      const user = useSelector((state: RootState) => state.user);
    
      useEffect(() => {
        const fetchDiaries = async () => {
          if (user) {
            http.get<null, Diary[]>(`diaries/${user.id}`).then((data) => {
              if (data && data.length > 0) {
                const sortedByUpdatedAt = data.sort((a, b) => {
                  return dayjs(b.updatedAt).unix() - dayjs(a.updatedAt).unix();
                });
                dispatch(addDiary(sortedByUpdatedAt));
              }
            });
          }
        };
        fetchDiaries();
      }, [dispatch, user]);
    
      const createDiary = async () => {
        const result = await Swal.mixin({
          input: 'text',
          confirmButtonText: 'Next →',
          showCancelButton: true,
          progressSteps: ['1', '2'],
        }).queue([
          {
            titleText: 'Diary title',
            input: 'text',
          },
          {
            titleText: 'Private or public diary?',
            input: 'radio',
            inputOptions: {
              private: 'Private',
              public: 'Public',
            },
            inputValue: 'private',
          },
        ]);
        if (result.value) {
          const { value } = result;
          const {
            diary,
            user: _user,
          } = await http.post<Partial<Diary>, { diary: Diary; user: User }>('/diaries/', {
            title: value[0],
            type: value[1],
            userId: user?.id,
          });
          if (diary && user) {
            dispatch(addDiary([diary] as Diary[]));
            dispatch(addDiary([diary] as Diary[]));
            dispatch(setUser(_user));
            return Swal.fire({
              titleText: 'All done!',
              confirmButtonText: 'OK!',
            });
          }
        }
        Swal.fire({
          titleText: 'Cancelled',
        });
      };
    
      return (
        <div style={{ padding: '1em 0.4em' }}>
          <Switch>
            <Route path="/diary/:id">
              <DiaryEntriesList />
            </Route>
            <Route path="/">
              <button onClick={createDiary}>Create New</button>
              {diaries.map((diary, idx) => (
                <DiaryTile key={idx} diary={diary} />
              ))}
            </Route>
          </Switch>
        </div>
      );
    };
    
    export default Diaries;
    

    In this component, we have a function to fetch the user’s diaries inside a useEffect hook, and a function to create a new diary. We also render our components in react-router’s <Route /> component, rendering a diary’s entries if its id matches the path param in the route /diary/:id, or otherwise rendering a list of the user’s diaries.

    To wrap things up, let’s update the Home.tsx component. First, update the imports to look like the following:

    import React, { FC } from 'react';
    import Diaries from '../diary/Diaries';
    import Editor from '../entry/Editor';
    

    Then, change the component’s return statement to the following:

    return (
      <div className="two-cols">
        <div className="left">
          <Diaries />
        </div>
        <div className="right">
          <Editor />
        </div>
      </div>
    

    Finally, replace the contents of the index.css file in your app’s src directory with the following code:

    :root {
      --primary-color: #778899;
      --error-color: #f85032;
      --text-color: #0d0d0d;
      --transition: all ease-in-out 0.3s;
    }
    body {
      margin: 0;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
        'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
        sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
    }
    html, body, #root {
      height: 100%;
    }
    *, *:before, *:after {
      box-sizing: border-box;
    }
    .auth {
      display: flex;
      align-items: center;
      height: 100%;
    }
    .card {
      background: #fff;
      padding: 3rem;
      text-align: center;
      box-shadow: 2px 8px 12px rgba(0, 0, 0, 0.1);
      max-width: 450px;
      width: 90%;
      margin: 0 auto;
    }
    .inputWrapper {
      margin: 1rem auto;
      width: 100%;
    }
    input:not([type='checkbox']), button {
      border-radius: 0.5rem;
      width: 100%;
    }
    input:not([type='checkbox']), textarea {
      border: 2px solid rgba(0, 0, 0, 0.1);
      padding: 1em;
      color: var(--text-color);
      transition: var(--transition);
    }
    input:not([type='checkbox']):focus, textarea:focus {
      outline: none;
      border-color: var(--primary-color);
    }
    button {
      appearance: none;
      border: 1px solid var(--primary-color);
      color: #fff;
      background-color: var(--primary-color);
      text-transform: uppercase;
      font-weight: bold;
      outline: none;
      cursor: pointer;
      padding: 1em;
      box-shadow: 1px 4px 6px rgba(0, 0, 0, 0.1);
      transition: var(--transition);
    }
    button.secondary {
      color: var(--primary-color);
      background-color: #fff;
      border-color: #fff;
    }
    button:hover, button:focus {
      box-shadow: 1px 6px 8px rgba(0, 0, 0, 0.1);
    }
    .error {
      margin: 0;
      margin-top: 0.2em;
      font-size: 0.8em;
      color: var(--error-color);
      animation: 0.3s ease-in-out forwards fadeIn;
    }
    .two-cols {
      display: flex;
      flex-wrap: wrap;
      height: 100vh;
    }
    .two-cols .left {
      border-right: 1px solid rgba(0, 0, 0, 0.1);
      height: 100%;
      overflow-y: scroll;
    }
    .two-cols .right {
      overflow-y: auto;
    }
    .title {
      font-size: 1.3rem;
    }
    .subtitle {
      font-size: 0.9rem;
      opacity: 0.85;
    }
    .title, .subtitle {
      margin: 0;
    }
    .diary-tile {
      border-bottom: 1px solid rgba(0, 0, 0, 0.1);
      padding: 1em;
    }
    .editor {
      height: 100%;
      padding: 1em;
    }
    .editor input {
      width: 100%;
    }
    .editor textarea {
      width: 100%;
      height: calc(100vh - 160px);
    }
    .entries ul {
      list-style: none;
      padding: 0;
    }
    .entries li {
      border-top: 1px solid rgba(0, 0, 0, 0.1);
      padding: 0.5em;
      cursor: pointer;
    }
    .entries li:nth-child(even) {
      background: rgba(0, 0, 0, 0.1);
    }
    
    @media (min-width: 768px) {
      .two-cols .left {
        width: 25%;
      }
      .two-cols .right {
        width: 75%;
      }
    }
    @keyframes fadeIn {
      0% {
        opacity: 0;
      }
      100% {
        opacity: 0.8;
      }
    }
    

    That’s it! You can now run npm start or yarn start and check out the final app at http://localhost:3000.

    Final App Home Screen (Unauthenticated User). (Large preview)

    Conclusion

    In this guide, you have learned how to rapidly develop applications using Redux. You also learned about good practices to follow when working with Redux and React, in order to make debugging and extending your applications easier. This guide is by no means extensive as there are still ongoing discussions surrounding Redux and some of its concepts. Please check out the Redux and React-Redux docs if you’d like to learn more about using Redux in your React projects.

    References

    Smashing Editorial
    (yk)

    Source link