GithubHelp home page GithubHelp logo

ctk41 / react-kata Goto Github PK

View Code? Open in Web Editor NEW

This project forked from dmcruz/react-kata

0.0 0.0 0.0 10.21 MB

Create react app from scratch walkthrough. Goal is to cover router, fetching API, redux, sagas, unit tests

Home Page: https://dmcruz.github.io/react-kata

License: MIT License

TypeScript 92.63% CSS 3.64% HTML 3.73%

react-kata's Introduction

React Kata

Overview

Learn react by example. In this walkthrough we will be building a SW Manager portal, where we can view people and starships from the Star Wars universe. We will be utilizing the Star Wars API (SWAPI) which is publicly available without authentication. Later we will enhance this app by building your squad and adding starships in hangar. Don't expect too much, this will be a simple app.

Each branch of this repository will represent a part of this tutorial. In order to avoid spoilers to exercises, avoid running the master branch, but that is up to you.

  1. module-1: Installation, setup, adding UI component library (Ant Design), adding router
  2. module-2: Adding style, Fetching API, useState hook, rendering state
  3. module-3: Promise.all, Component and Props
  4. module-4: Redux state management, useDispatch and useSelector hooks
  5. module-5: tackling exercises in Module 4, useEffect
  6. module-6: generator function basics and Redux Sagas
  7. module-7: withLoading Higher Order Component (HOC); unit testing redux, sagas and components
  8. module-8: Wrap up; the app is now completed.

Demo

Finished product is accessible here: https://dmcruz.github.io/react-kata/

Notes

If you don't want to follow the walkthrough and just want to run the app, follow the steps.

  1. npm install
  2. npm start
  3. Open in browser: http://localhost:3000/react-kata/

Module 1

Prerequisites

  1. Install nodejs
  2. Install VSCode

Setup

  1. Boiler plate creation

Create a default typescript based react app using npx

npx create-react-app my-app --template typescript

  1. Open the project using VSCode
cd my-app
code .
  1. Since the project has been bootstrapped from create-react-app the dependencies have been installed. All you have to do is run npm start in terminal to run the app.

    Note: run npm install before npm start if you didn't start from scratch.

  2. Once the app has been compiled successfully, access the URL in your browser: http://localhost:3000

  3. You can open another instance of a terminal and run tests.

To run tests:

npm run test

To run a specific test file:

npm run test src/App.test.tsx

To run test with coverage:

npm run test -- --coverage src/App.test.tsx

Optional: VSCode extensions

  1. Install recommended VSCode extensions
  • Prettier
    • Follow this guide to configure VSCode to prettify (autoformat) the codes on save: https://glebbahmutov.com/blog/configure-prettier-in-vscode/
    • Alternatively, here's the simplified instructions
      • Install prettier VSCode extension
      • Install prettier: npm install --save-dev --save-exact prettier
      • In this repository you can refer to prettierrc.json and .vscode/settings.json and add them in your project
      • Saving should automatically indent, or apply the rules set in prettierrc.json.

Adding dependencies

The built-in template does not come with a UI library and routing solution.

To add Ant Design:

npm i --save antd

To add router:

npm i --save react-router-dom

Refer to the docs:

Ant Design style

  1. Import the ant design stylesheet manually by adding this import statement on App.tsx

import "antd/dist/antd.css";

Create a Layout

In this section we will create the layout of our website. It will be a simple layout: Header which will contain the menu, Content to render the page contents, and Footer.

  1. Under src folder create components folder.
  2. Under src/components folder, create layout folder.
  3. Under layout folder, create MyLayout.tsx.
import { Layout, Menu } from 'antd';
const { Header, Content, Footer } = Layout;

const MyLayout: React.FC = ({ children }) => {
  return (
    <Layout className="layout">
      <Header>
        <Menu theme="dark" mode="horizontal">
          <Menu.Item key="home" title="Home">
            Home
          </Menu.Item>
          <Menu.Item key="nav1" title="Nav 1">
            Nav 1
          </Menu.Item>
          <Menu.Item key="nav2" title="Nav 2">
            Nav 2
          </Menu.Item>
        </Menu>
      </Header>
      <Content style={{ padding: '0 50px', minHeight: '50px' }}>
        {children}
      </Content>
      <Footer>React Kata &copy; 2021</Footer>
    </Layout>
  );
};

export default MyLayout;
  1. Modify App.tsx, remove all the code. Import MyLayout and render it.
import MyLayout from './components/layout/MyLayout';

import 'antd/dist/antd.css';

function App() {
  return <MyLayout />;
}

export default App;

Create content pages

Create pages with the aim to render content specific to each.

  1. Under components, create Home.tsx
const Home = () => {
  return (
    <div>
      <h1>Home</h1>
    </div>
  );
};
export default Home;
  1. Under components, create People.tsx
const People = () => {
  return (
    <div>
      <h1>People</h1>
    </div>
  );
};
export default People;

Add Routing

Add routing in menu and render content according to selected link.

  1. Modify MyLayout.tsx, import Link from react-router-dom in order to define hyperlinks.

import { Link } from "react-router-dom";

  1. Enclose the Menu Items with <Link> and pointing to path

Partial snippet shows the modified part of MyLayout.tsx:

    <Menu.Item key="home" title="Home">
      <Link to="/">Home</Link>
    </Menu.Item>
    <Menu.Item key="people" title="People">
      <Link to="/people">People</Link>
    </Menu.Item>

Full snippet:

import { Link } from 'react-router-dom';
import { Layout, Menu } from 'antd';
const { Header, Content, Footer } = Layout;

const MyLayout: React.FC = ({ children }) => {
  return (
    <Layout className="layout">
      <Header>
        <Menu theme="dark" mode="horizontal">
          <Menu.Item key="home" title="Home">
            <Link to="/">Home</Link>
          </Menu.Item>
          <Menu.Item key="people" title="People">
            <Link to="/people">People</Link>
          </Menu.Item>
          <Menu.Item key="nav2" title="Nav 2">
            Nav 2
          </Menu.Item>
        </Menu>
      </Header>
      <Content style={{ padding: '0 50px', minHeight: '50px' }}>
        {children}
      </Content>
      <Footer>React Kata &copy; 2021</Footer>
    </Layout>
  );
};

export default MyLayout;
  1. Modify App.tsx and add import BrowserRouter, Routes (previously Switch in react-router-dom v5), Route.

import { BrowserRouter as Router, Routes, Route } from "react-router-dom";

  1. Import Home and People components in App.tsx.
import Home from './components/Home';
import People from './components/People';
  1. Modify App.tsx rendered component to:
<Router>
  <MyLayout>
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/people" element={<People />} />
    </Routes>
  </MyLayout>
</Router>

What it does is render the component according the route. If URL is / it will render Home in the content area. If URL is /people, People will be rendered in the content area.

  1. Open http://localhost:3000/, click the links to observe the changes.

Module 2

Refer to branch module-2 for the source code.

Adding Styles

  1. In src/components/layout, create MyLayout.css
.site-content {
  background-color: #fff;
  min-height: 280px;
  padding: 25px;
}
  1. Modify MyLayout.tsx and import the css that was just created.
import './MyLayout.css';
  1. Still in MyLayout.tsx, enclose the {children} elements with div referring to site-content class.
<div className="site-content">{children}</div>

The result should be the content area will have a white background color.

Fetching API

In this section we will be using fetch which is a built in function in modern browsers. It has the ability to fetch resources across the network.

Read more here: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API https://reactjs.org/docs/faq-ajax.html

We will be using Star Wars API (SWAPI) for RestAPI resources.

Read more here: https://swapi.dev/documentation

  1. Create PeopleList.tsx under /src/components/people. The goal is to create a button that when clicked will request RestAPI.

    1.1 Import Button from antd.

      import { Button } from "antd";
    

    1.2 Render the Button.

    return <Button type="primary">Fetch Data</Button>;

    1.3 Add an onClick handler for the button. This will trigger a fetch request and print the results in the console. Inspect the results to understand the response of the API.

    const handleClickFetch = (e: any) => {
      fetch('https://swapi.dev/api/people')
        .then((res: Response) => res.json())
        .then(
          (data: any) => {
            console.log(data);
          },
          (error) => {
            console.error(error);
          }
        );
    };
    
    return (
      <Button type="primary" onClick={handleClickFetch}>
        Fetch Data
      </Button>
    );

    Full snippet of PeopleList.tsx:

    import { Button } from 'antd';
    const PeopleList = () => {
      const handleClickFetch = (e: any) => {
        fetch('https://swapi.dev/api/people')
          .then((res: Response) => res.json())
          .then(
            (data: any) => {
              console.log(data.results);
            },
            (error) => {
              console.error(error);
            }
          );
      };
    
      return (
        <div>
          <div>
            <Button type="primary" onClick={handleClickFetch}>
              Fetch Data
            </Button>
          </div>
        </div>
      );
    };
    export default PeopleList;
  2. Modify People.tsx and import PeopleList and render it.

    import PeopleList from './people/PeopleList';
    
    const People = () => {
      return (
        <div>
          <h1>People</h1>
          <PeopleList />
        </div>
      );
    };
    export default People;
  3. Check the browser and click the button. On the browser's console you should see the response from the API.

Render State

Now let's render the result on the screen. For that we will be utilising local state to store the data. Functional components are stateless components. In order to use state, we have to add useState hook from react.

Read more: https://reactjs.org/docs/hooks-state.html

  1. Import useState from react

    import { useState } from "react";

  2. useState() is a function that accepts initial state and returns 2 values: the current state and the function that updates it. What we need is to retrieve a list of people from SWAPI People api and return to the screen. For that we will construct a list state object and initialize it to an empty array.

    const [list, setList] = useState([]);

  3. Call setList and pass the results obtained from the API. Inspecting SWAPI People result, list is returned under results property of the response.

    fetch('https://swapi.dev/api/people')
      .then((res: Response) => res.json())
      .then(
        (data: any) => {
          setList(data.results); // updated here
        },
        (error) => {
          console.error(error);
        }
      );
  4. Now we render the results on the component using the list state object. In react, when rendering multiple items, they have to be uniquely identified with a key. We will use map function to return a rendered item. For now we only display the name of the Star Wars person. We use the built in index parameter of map to pass to key.

    Read more: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map

    const viewPerson = list.map((item: any, index: number) => (
      <li key={index}>{item.name}</li>
    ));
  5. Update the rendered item in the screen, render viewPerson. Save the changes.

    return (
      <div>
        <div>
          <Button type="primary" onClick={handleClickFetch}>
            Fetch Data
          </Button>
        </div>
        <div>
          <ul>{viewPerson}</ul>
        </div>
      </div>
    );
  6. Check the browser, click on People menu. Click Fetch and you should see a list of Star Wars person in the screen.

Module 3

In this module, we will be learning about Promise.all, and passing props to child components.

Calling multiple APIs at once using Promise.all

In Module 2, we learned about fetching API. If you notice, the API returns only the first page. What if we want to retrieve all data at once?

For that refer to module-3 branch of this repository and browse to /src/services/FetchHelper.ts which has getAllPeople() function.

To consume the data in this function, here's a sample code:

(async () => {
  try {
    const people = await FetchHelper.getAllPeople();
  } catch (error: any) {
    console.error(`${error}`);
  }
})();

Read more:

It is also possible to call fetch multiple times, this is just to show how to use Promise.all.

Exercise: Create PersonRow Component and Pass Props

Time for some exercise. If in doubt you can always check module-3 branch in this repository.

The goal here is to learn about parent and child component communication through props. https://reactjs.org/docs/components-and-props.html

  1. In the previous module, we displayed a list of names which is rendered by viewPerson embedded in PeopleList.tsx.
  2. Extract the viewPerson component into a new component PersonRow.tsx and render the person attributes in tabular form. Use Row and Col components of Ant Design. https://ant.design/components/grid/
  3. Pass the person attributes in PersonRow as properties.

Module 4

In the Module 2, we learned about local state. But what if we need to share this data in another component. For that, we will be using Redux which can manage global state.

In this module, the goal is to setup Redux, and apply it.

References:

Setup Redux

1. Add dependencies

Add redux, react-redux, and redux-logger dependencies by running the following command:

npm i --save redux react-redux redux-logger

Add @types/redux-logger as a dev dependency:

npm i --save-dev @types/redux-logger

2. Create Redux objects (reducer, action, store)

The goal is to create a global state for people list.

  1. First, create /src/redux folder. Anything that has to do with redux will be placed here.

  2. We will group reducers per feature folder like /src/redux/{feature}.

    Under feature folder we will be creating redux objects:

    • action - an object that represents an intention to change the state. Action must specify a type field and although optional another parameter is payload or the "new value" to be sent. All action objects for the feature are placed here.

    • reducer - a function that accepts a previous state and an action as inputs and returns the new state. Reducers are the one that knows how to modify the state. All reducer objects for the feature are placed here.

  3. Let's start creating our People reducer. Create /src/redux/people folder.

  4. Create people reducer file /src/redux/people/people.reducer.ts

    INITIAL_STATE defines the model of your state. Any data that has to be observed or changed will go here.

    const INITIAL_STATE = {
      list: [],
    };
    
    const peopleReducer = (state = INITIAL_STATE, action: any) => {
      switch (action.type) {
        case 'SET_PEOPLE_LIST':
          return {
            ...state,
            list: action.payload,
          };
        default:
          return state;
      }
    };
    
    export default peopleReducer;
  5. Create an action file in the same path people.action.ts

    export const setPeople = (list: any) => ({
      type: 'SET_PEOPLE_LIST',
      payload: list,
    });
  6. Create store file /src/redux/store.ts

    6.1 Import createStore, applyMiddleware, combineReducers from redux

    6.2 Import logger middleware from redux-logger. Logger will be used in development mode so that changes to the global state is logged in browser's console.

    6.3 Import the people reducer that we created and build the reducer object.

    6.4 Create the store object and export it.

    import { createStore, applyMiddleware, combineReducers } from 'redux';
    import logger from 'redux-logger';
    import peopleReducer from './people/people.reducer';
    
    const middlewares = [];
    
    if (process.env.NODE_ENV === 'development') {
      middlewares.push(logger);
    }
    
    const rootReducer = combineReducers({
      people: peopleReducer,
    });
    
    const store = createStore(rootReducer, applyMiddleware(...middlewares));
    export default store;

3. Connect the store to our application

We will be connecting the store object into all our components. We do this using Provider component from react-redux. Provider will wrap around the entire application in order for all the components under it to have access to the store object.

  1. Modify src/index.js. Import Provider from react-redux.

    import { Provider } from 'react-redux';

  2. Import store

    import store from './redux/store';

  3. Enclose App with Provider component, passing store as a prop.

    <Provider store={store}>
      <App />
    </Provider>
  4. Refer to /src/index.tsx in module-4 branch for the changes.

Using the Redux State

Let's go back to /src/components/people/PeopleList.tsx and remove the local states and use global state instead.

  1. Remove this import: import { useState } from "react";

  2. Remove this line: const [list, setList] = useState([]);

  3. Remove the usage of setList in handleClickFetch.

  4. We will be needing 2 hooks from react redux in order to dispatch a change action and extract a value from the global state. We accomplish this by using useDispatch() and useSelector hooks. Read more here: https://react-redux.js.org/api/hooks

    import { useDispatch, useSelector } from "react-redux";

  5. Create a dispatch object and create a list object to extract the people list from the global state.

    const dispatch = useDispatch();
    const list: [] = useSelector((state: any) => state.people.list);
  6. Import setPeople action from /src/redux/people/people.action

    import { setPeople } from '../../redux/people/people.action';
  7. In the Fetch Data click button handler, dispatch the setPeople action and pass the result of the API here.

    const peopleList = await FetchHelper.getAllPeople();
    dispatch(setPeople(peopleList));
  8. Save the changes and check if the fetch data still works and list is rendered.

  9. Inspect the browser console, and see that redux logger is logging whenever there's a change in the state.

Reuse the State in Another Page

What we have done in the previous section is replace local state with global state. Let's prove that the state can be reused outside the page the state was initially created.

We will be modifying /src/components/Home.tsx and show a random person when it's available.

  1. SWAPI People API returns a maximum of 82 records. Create a random number with that maximum index.

  2. Extract the people list and pass the random index.

    const randomNumber = Math.floor(Math.random() * 82);
    const randomPerson = useSelector(
      (state: any) => state.people.list[randomNumber]
    );
  3. Render the randomPerson value. Since Home is rendered first before fetching the People list, use the optional chaining operator so that in case there is no value in the state, it will not cause a runtime exception in rendering.

    return (
      <div>
        <h1>Home</h1>
        {randomPerson?.name}
      </div>
    );
  4. Save the changes and reload Home. At first it should be empty. Transfer to People and fetch data, once data is fetched, go back to Home and a random person name should appear.

Exercise 1: Create a Random Person Widget

Create a random person widget that will display all attributes available in a presentable way. Render this in Home. I will create this component in the next module.

Exercise 2: Create Starships Page and Redux State

SWAPI Starships API: https://swapi.dev/api/starships/

Create a new route to /starships that will render Starship list on load. Use useEffect hook for this. Read more here: https://reactjs.org/docs/hooks-effect.html

Module 5

This module is more about tackling the exercises. There are a lot of changes so you can take a look at module-5 branch.

Added dependency:

  1. ts-md5 - for hashing md5 used for generating Gravatar identicon

    npm i --save ts-md5

Change Log

  1. Module 4 Exersise 1: RandomPerson widget to generate a featured person in Home page
  2. Module 4 Exercise 2: Starship component and redux created
  3. Generic getAll function is created in FetchHelper.ts
  4. People List is now a grid of cards

useEffect

useEffect hook is equivalent to componentDidMount, componentDidUpdate, and componentWillAmount lifecycle methods. When the component is loaded this effect is called.

There are 2 arguments in this function. First argument is the behavior or function, second argument is the array of dependencies. React will compare previous value of dependency with the new value to trigger if it needs to perform the effect function. If second argument is an empty array, it will invoke useEffect once only.

The following snippet retrieves Starships API on load. This is only done once because the second argument is an empty array.

  useEffect(() => {
    (async () => {
      try {
        const list = await FetchHelper.getAll(SwapiUrls.STARSHIPS);
        dispatch(setStarships(list));
      } catch (error: any) {
        message.error(`${error}`);
      }
    })();
    // eslint-disable-next-line
  }, []);

Module 6

Goal in this module is to apply redux sagas. Learn more about it here:

Generator Functions

The foundation of redux saga is generator functions. Learn more about generator function: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*

Example of a generator function:

function* fetchStarshipsAsync() {
  const list = yield FetchHelper.getAll(SwapiUrls.STARSHIPS);
  yield put(setStarships(list));
}

In generator function, each yielded function is awaited before proceeding to the next one. In the example, result of Starships API is awaited first and set to list before the dispatching of setStarships is done. Calling this function does not execute immediately, it has to be iterated by using next(). Saga middleware takes care of handling this.

// this will not do anything and will be paused (suspended)
fetchStarshipsAsync();

// this will execute the yielded functions
const myFunc = fetchStarshipsAsync();
myFunc.next();
myFunc.next();

Common Pattern in Saga

As you have noticed API calls exists in the component such as PeopleList and StarshipList. We will be taking out these API calls out of components and delegate them to sagas. This will result in cleaner code and clear separation of concern: components are for rendering while sagas are for managing side effects.

Currently, PeopleList fetches People API on load. Our target will be for PeopleList to dispatch a request for People API on load. A saga watcher function will trigger the API when it receives a fetch request. We will be also adding a loading indicator when fetching data is in progress.

Let's imagine 3 events:

  1. requestStart - this is where loading will be set to true as it marks that the fetch is in progress. This will trigger the request to fetch API.
  2. requestSuccess - this event indicates that the API has returned the response data, and loading should be set to false.
  3. requestError - this event indicates an error during API request, so we can obtain error message here, and loading should be set to false.

You will find this is a common pattern we will be applying in sagas.

Setup Redux Saga

  1. Add dependency

    npm install --save redux-saga

  2. Modify people.reducer.ts. Add loading (set to false) in INITIAL_STATE. Create 3 action types: FETCH_PEOPLE_START, FETCH_PEOPLE_SUCCESS, FETCH_PEOPLE_ERROR.

    Partial Snippet:

    const INITIAL_STATE = {
      list: [],
      loading: false,
    };
    
    // ... code redacted
    
    case 'FETCH_PEOPLE_START':
      return {
        ...state,
        loading: true,
      };
    case 'FETCH_PEOPLE_SUCCESS':
      return {
        ...state,
        loading: false,
        list: action.payload,
      };
    case 'FETCH_PEOPLE_ERROR':
      return {
        ...state,
        loading: false,
        error: action.payload,
      };
  3. Create 3 actions corresponding to previous step in people.action.ts.

    Snippet:

    export const fetchPeopleStart = () => ({
      type: 'FETCH_PEOPLE_START',
    });
    
    export const fetchPeopleSuccess = (list: []) => ({
      type: 'FETCH_PEOPLE_SUCCESS',
      payload: list,
    });
    
    export const fetchPeopleError = (error: any) => ({
      type: 'FETCH_PEOPLE_ERROR',
      payload: error,
    });
  4. Create the People saga under /src/redux/people/people.saga.ts.

    import { call, put, takeLatest } from 'redux-saga/effects';
    import { FetchHelper } from '../../services/FetchHelper';
    import { SwapiUrls } from '../../services/SwapiUrls';
    import { fetchPeopleError, fetchPeopleSuccess } from './people.action';
    
    // This is a worker function responsible for fetching API and bind data to state if success or bind error if fail
    function* fetchPeopleAsync(): Generator<any, any, any> {
      try {
        const people = yield call(FetchHelper.getAll, SwapiUrls.PEOPLE);
        yield put(fetchPeopleSuccess(people));
      } catch (e: any) {
        yield put(fetchPeopleError(e.message || e));
      }
    }
    
    // This is a watcher function, once FETCH_PEOPLE_START is received, it will trigger fetchPeopleAsync
    export function* watchFetchPeopleStart() {
      yield takeLatest('FETCH_PEOPLE_START', fetchPeopleAsync);
    }
  5. Create root-saga.ts under /src/redux/root-saga.ts. This will be the place we will aggregate multiple sagas parallely. For now we have 1 saga, but should we have more we just add here.

    import { all, call } from 'redux-saga/effects';
    import { watchFetchPeopleStart } from './people/people.saga';
    
    export default function* rootSaga() {
      yield all([call(watchFetchPeopleStart)]);
    }
  6. Add redux saga middleware in our store.ts.

    6.1 Import createSagaMiddleware

    import createSagaMiddleware from 'redux-saga'
    

    6.2 Import rootSaga

    import rootSaga from './root-saga';
    

    6.3 Create sagaMiddleware and add it in middlewares.

    const sagaMiddleware = createSagaMiddleware();
    const middlewares: any = [sagaMiddleware];
    

    6.4 Run the saga

    sagaMiddleware.run(rootSaga);
    

    6.5 You may refer to store.ts source code in module-6 branch of this repository for the complete code.

Modify Component to Dispatch the Triggering Saga Function

  1. Modify PeopleList.tsx, import fetchPeopleStart action and replace the useEffect function like this:

    useEffect(() => {
      dispatch(fetchPeopleStart());
    }, []);
  2. Save and reload the app. If everything is setup correctly, the data will load as usual. If not, refer to module-6 branch and check if you missed anything.

Refactoring

  1. Since setPeople action or SET_PEOPLE_LIST is no longer in use, it has been removed from people.action.ts and people.reducer.ts.

Adding Loading Indicator

  1. Modify PeopleList.tsx, use useSelector hook to get the loading state and show a loading indicator. To do this, you can enclose the area you want to cover with <Skeleton loading={loading} avatar active></Skeleton>.
  2. Refresh People page and you should see a shimmer on load before the data is loaded.

Exercise: Create the saga for Starships

Challenge yourself to create the saga for Starships. The solution for this exercise will be available in Module 7.

Module 7

Change Log

  1. Loading of people and starships is now done in MyLayout.tsx since this is the master page. Whether you start reloading from home, people or starships, data will still load.
  2. Added loading indicator in RandomPerson.tsx
  3. Starships saga
  4. Added unit tests

withLoading Higher Order Component (HOC)

Loading has been prevalent in components that require fetching API. Since this is common, let's create a wrapper component or a higher order component (HOC) that will inject the required loading state and render the loading indicator.

You can check out /src/components/wrapper/withLoading.tsx, /src/components/wrapper/withLoadingPeople.tsx, /src/components/wrapper/withLoadingStarships.tsx for the code to accomplish this.

withLoading renders the Skeleton loading indicator when loading property is true.

withLoadingPeople extracts the loading state from people reducer and connects the wrapped component to redux. People components that need loading indicator should use this.

withLoadingStarships does the same but for Starships components.

Using these HOC is simple, in RandomPerson.tsx, we modify the exported component and surround it with the HOC function such as: export default withLoadingPeople(RandomPerson);.

Unit testing Redux and Sagas

  1. Added dev dependency on redux-saga-test-plan. To install it:

    npm i --save-dev redux-saga-test-plan

  2. Samples to check from module-7 branch:

    • /src/redux/people/people.reducer.test.ts
    • /src/redux/people/people.saga.test.ts

Read more on redux-saga-test-plan: https://github.com/jfairbank/redux-saga-test-plan

  1. Running the tests

    npm run test -- --coverage
    npm run test -- --coverage src/redux/people/people.reducer.test.ts
    npm run test -- --coverage src/redux/people/people.saga.test.ts
    

Unit testing Components

  1. Added dev dependency to sinon. To install it:

    npm i --save-dev sinon npm i --save-dev @types/sinon

  2. Samples to check

    /src/components/widget/Gravatar.test.tsx - stateless component, easy to test /src/components/widget/RandomPerson.test.tsx - useSelector hook should be mocked, Math.random function also in order to return a deterministic result

  3. Run the tests

    npm run test -- --coverage src/components/widget/Gravatar.test.tsx
    npm run test -- --coverage src/components/widget/RandomPerson.test.tsx
    

Exercise: Create Unit Tests

  1. Study the tests done for people reduer and saga and create the tests for starship redux.
  2. Create unit tests for other components

Extra challenge: add more features

  1. Create a Squad widget and display in home base

    As a captain, you are building a squad of 10 people. Create a way to add or remove people in your squad, once the slot has been filled up it will not be possible to add more people. Once the person is in your squad he can no longer be recruited unless you remove him.

  2. Create a Hangar widget and display in home base

    You have a hangar with a capacity for 10 starships, create a way to add or remove starships in your hangar. You can have mutiple starships of the same model, but you are limited to 10. By the way you can't add Death Star, dream on.

Module 8

This module completes the SW Manager Portal. Squad and Hangar widgets have been added. Home Base state is also added to manage your resources.

You can study the code in module-8 branch to learn more.

Added dependencies

  1. Reselect - a good way to derive the state values. Read more: https://github.com/reduxjs/reselect

    npm i --save reselect

Extra Challenge

Feel free to enhance and add more features. May the force be with you.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.