boilerplate-react-context-hooks icon indicating copy to clipboard operation
boilerplate-react-context-hooks copied to clipboard

Small boilerplate for testing react without redux, but with context and hooks

Simple Boilerplate for React Context and Hooks

Small boilerplate for testing react without redux, but with context and hooks

alt text

Prior

  • install globally brew, nodejs and yarn:
    • brew
    • brew install nodejs yarn
  • create a repo named "boilerplate-react-context-hooks" on git without licence

Creation of basic react boilerplate

  • yarn add react-create-app
  • cd Workspace && yarn create react-app boilerplate-react-context-hooks
  • cd boilerplate-react-context-hooks && yarn start

info : Create React App.

Available Scripts : yarn start|test|buikd|eject

You should see in chrome:

alt text

GITHUB

  • git init
  • git remote add origin https://github.com/nicolastrote/boilerplate-react-context-hooks.git
  • git push --set-upstream origin master

App.jsx and SCSS

  • cd src
  • remove logo : rm logo.svg App.test.js
  • use jsx instead of js: mv App.js App.jsx
  • use scss instead of css: mv App.css App.scss
  • for scss node need node-sass: yarn add node-sass

re-write App.jsx as:

import React from 'react';

import TodoForm from './components/TodoForm';
import TodoList from './components/TodoList';
import './App.scss';

export default function App() {
  return (
    <div className="App">
      <h1>Todo App</h1>
      <div>
        <TodoForm />
        <TodoList />
      </div>
    </div>
  )
}

Components TodoForm & TodoList

We will create 2 components, one for the general form of adding Todos and a 2d form for listing all todos. All information are coming from local state for now. We use react hooks for local state manipulation.

  • cd src
  • mkdir components && cd components
  • mkdir TodoForm && touch TodoForm/index.jsx
  • mkdir TodoList && touch TodoList/index.jsx

write src/components/TodoForm/index.jsx as

import React, { useState, useContext } from 'react';

export default function TodoForm() {
    const [todo, setTodo] = useState('');

    const handleTodoInput = (event) => {
        setTodo(event.target.value);
    };

    // submits todo to context store and set todo form input to blank
    const handleTodoSubmit = (event) => {
        event.preventDefault();
        todo.trim().length > 0 && console.log(todo);
        setTodo('');
    };

    return (
        <form onSubmit={handleTodoSubmit} className="todo-form">
            <input type="text" value={todo} onChange={handleTodoInput} />
            <button type="submit">Add Todo</button>
        </form>
    )
}

write src/components/TodoList/index.jsx as

import React, { useContext } from 'react';

export default function TodoList() {
    return <div className="todo-list">todo list</div>
};

STORE

We will create a file for managing the store with context:

  • cd src && mkdir store && cd store && touch Store.jsx

Write Store.jsx as

import React, { createContext, useReducer } from 'react';

const initialState = {}

export const Store = createContext(initialState)

const reducer = (state, action) => {
  switch (action.type) {
    default:
      return state
  }
}

export const StoreProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState)

  return <Store.Provider value={{ state, dispatch }}>{children}</Store.Provider>
}

INDEX.JSX

Now we need to wrap App component with StoreProvider for using context everywhere

  • Re-write index.jsx as
import React from 'react'
import ReactDOM from 'react-dom'

import './index.scss'
import App from './App'
import { StoreProvider } from './store/Store';

const app = (
    <StoreProvider>
        <App />
    </StoreProvider>
);

ReactDOM.render(app, document.getElementById('root'));

Reducer with Context API

Now we need to set reducers for storing information in our "store context".

re-write src/store/Store.jsx as

import React, { createContext, useReducer } from 'react';

const initialState = {
    todoList: [],
};

export const Store = createContext(initialState);

const addTodo = (state, todoText) => {
    const newTodo = {
        id:
            (state.todoList.length > 0 && state.todoList[state.todoList.length - 1].id + 1) ||
            0,
        text: todoText,
        tag: 'active',
    };

    return { ...state, todoList: [...state.todoList, newTodo] };
};

const removeTodo = (state, todoId) => {
    const newTodoList = state.todoList.filter(todo => todo.id !== todoId);

    return { ...state, todoList: newTodoList }
};

const editTodoTag = (state, todoId, tag) => {
    const todo = state.todoList.find(todo => todo.id === todoId);
    const todoIndex = state.todoList.indexOf(todo);
    const newTodo = { ...todo, tag };

    const newTodoList = [
        ...state.todoList.slice(0, todoIndex),
        newTodo,
        ...state.todoList.slice(todoIndex + 1),
    ];

    return { ...state, todoList: newTodoList }
};

const reducer = (state, action) => {
    switch (action.type) {
        case 'ADD_TODO':
            return addTodo(state, action.todoText);
        case 'REMOVE_TODO':
            return removeTodo(state, action.todoId);
        case 'EDIT_TODO_TAG':
            return editTodoTag(state, action.todoId, action.tag);
        default:
            return state;
    }
};

export const StoreProvider = ({ children }) => {
    const [state, dispatch] = useReducer(reducer, initialState);

    return <Store.Provider value={{ state, dispatch }}>{children}</Store.Provider>;
};

ACTIONS

As we build reducers, now we can code actions.

  • cd src/store && touch Actions.jsx write Actions.jsx as
export const addTodo = (todo, dispatch) => {
    dispatch({
        type: 'ADD_TODO',
        todoText: todo,
    })
};

export const removeTodo = (todoId, dispatch) => {
    dispatch({
        type: 'REMOVE_TODO',
        todoId,
    })
};

export const editTag = (todoId, tag, dispatch) => {
    dispatch({
        type: 'EDIT_TODO_TAG',
        todoId,
        tag,
    })
};

Connect Global State with Components

Let's connect our reducer with components. re-write src/components/TodoForm/index.jsx as

import React, { useState, useContext } from 'react';

import { addTodo } from '../../store/Actions';
import { Store } from '../../store/Store';

export default function TodoForm() {
    const { dispatch } = useContext(Store);
    const [todo, setTodo] = useState('');

    const handleTodoInput = (event) => {
        setTodo(event.target.value)
    };

    const handleTodoSubmit = (event) => {
        event.preventDefault();
        todo.trim().length > 0 && addTodo(todo, dispatch);
        setTodo('');
    };

    return (
        <form onSubmit={ handleTodoSubmit } className="todo-form">
            <input type="text" value={ todo } onChange={ handleTodoInput } />
            <button type="submit">Add Todo</button>
        </form>
    )
}

re-write src/components/TodoList/index.jsx as

import React, { useContext } from 'react';

import Todo from './Todo';
import { Store } from '../../store/Store';

export default function TodoList() {
    const { state, dispatch } = useContext(Store);

    const todoList = state.todoList.map(todo => (
        <Todo
            key={todo.id}
            todoId={todo.id}
            todoText={todo.text}
            todoTag={todo.tag}
            dispatch={dispatch}
        />
    ));

    return <div className="todo-list">{todoList}</div>;
}

create Todo component

  • cd components/TodoList/ && touch Todo.jsx
  • write Todo.jsx as:
import React, { memo } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {faTrash} from "@fortawesome/free-solid-svg-icons/faTrash";

import { editTag, removeTodo } from '../../actions';

import './Todo.scss';

export const Todo = memo(
    (props) => {
        const { todoId, todoText, todoTag, dispatch } = props;

        const handleTodoTag = () => {
            const newTag = todoTag === 'active' ? 'complete' : 'active';
            editTag(todoId, newTag, dispatch);
        };

        const handleRemoveTodo = () => {
            removeTodo(todoId, dispatch)
        };

        const todoStyle = {
            color: (todoTag === 'complete' && '#90a4ae') || '#01579b',
            textDecoration: (todoTag === 'complete' && 'line-through') || 'none',
        };

        return (
            <div className="todo">
              <span style={todoStyle} onClick={handleTodoTag}>
                {todoText}
              </span>
              <button onClick={handleRemoveTodo}><FontAwesomeIcon icon={faTrash} size="lg"/></button>
             </div>
        )
    }
);

IMPROVEMENT

  • all architecture moved for an organisation more atomic as container folder, component folder, redux file, action file, store file.
  • use of barrels (ir: index.jsx file in folder)
  • add of react-fontawesome package for icon: yarn add @fortawesome/free-solid-svg-icons @fortawesome/react-fontawesome @fortawesome/fontawesome-svg-core