a python/javascript/php/linux guru ready to take on the world...
1 min read
We will start by creating some action type constants
export const ADD_TASK = 'ADD_TASK'export const TOGGLE_COMPLETE = 'TOGGLE_COMPLETE'export const TOGGLE_ARCHIVED = 'TOGGLE_ARCHIVED'
Now lets create an empty react context, so the rest of our app can share the values
import React, { createContext } from 'react'export const TaskContext = createContext({tasks: [],displayableTasks: [],addTaskInput: '',resetAddTaskInput: () => {},addTask: () => {},toggleComplete: () => {},toggleArchived: () => {},setAddTaskInput: () => {},})
The next step is to write a redux style reducer to update the state of our component
Which we will use with the React.useReducer
hook soon
The main things we do here, is map our action types to functions that will update the state as needed.
import uuid from 'uuid/v4'import { ADD_TASK, TOGGLE_ARCHIVED, TOGGLE_COMPLETE } from './types'const addTask = text => ({text,date: new Date(),id: uuid(),complete: false,archived: false,})const addTaskToTasks = (text, tasks) => [...tasks, addTask(text)]const toggleComplete = task => ({...task,complete: !task.complete,})const completeTaskById = (taskId, tasks) =>tasks.map(task => (task.id !== taskId ? task : toggleComplete(task)))const toggleArchived = task => ({...task,archived: !task.archived,})const archiveTaskById = (taskId, tasks) =>tasks.map(task => (task.id !== taskId ? task : toggleArchived(task)))export default (state, { type, value } = {}) => {const returnValues = {[ADD_TASK]: addTaskToTasks,[TOGGLE_COMPLETE]: completeTaskById,[TOGGLE_ARCHIVED]: archiveTaskById,}if (type in returnValues) {return returnValues[type](value, state)}return state}
Now we need to tie all of that logic up into our own hook
Which we will call useTasks
import React, { useState, useReducer } from 'react'import { ADD_TASK, TOGGLE_COMPLETE, TOGGLE_ARCHIVED } from './types'import taskReducer from './task-reducer'const useTasks = (initialTasks = []) => {const [tasks, dispatch] = useReducer(taskReducer, initialTasks)const [addTaskInput, setAddTaskInput] = useState('')const resetAddTaskInput = () => {setAddTaskInput('')}const addTask = text => {dispatch({type: ADD_TASK,value: text,})resetAddTaskInput()}const toggleComplete = taskId => {dispatch({type: TOGGLE_COMPLETE,value: taskId,})}const toggleArchived = taskId => {dispatch({type: TOGGLE_ARCHIVED,value: taskId,})}return {tasks,resetAddTaskInput,addTask,toggleComplete,toggleArchived,addTaskInput,setAddTaskInput,}}export default useTasks
Now in we can start creating our component.
We will start by plugging our task stuff from
our useTasks
hook into our custom context.
After defining this component we will have 2 new components to write:
TaskHeading - for our task list header
TaskListBody - to actually display our tasks
import React from 'react'import { Flex, BorderBox } from '@primer/components'import { TaskContext } from './task-context'import TaskHeading from './task-heading'import TaskListBody from './tasklist-body'import useTasks from './tasks'const TaskList = () => {const {toggleComplete,toggleArchived,tasks,addTaskInput,setAddTaskInput,addTask,} = useTasks()const displayableTasks = tasks.filter(task => !task.archived)const value = {tasks,displayableTasks,toggleComplete,toggleArchived,addTask,addTaskInput,setAddTaskInput,}return (<TaskContext.Provider value={value}><BorderBox p={2} m={3}><TaskHeading /><BorderBox border={0} borderBottom={1} m={1} p={1}><Flex p={2} m={2} flexDirection={'column'}><TaskListBody /></Flex></BorderBox></BorderBox></TaskContext.Provider>)}export default TaskList
Now we need a heading to display how many tasks are in the list for the user
import React, { useContext } from 'react'import { Box, CounterLabel, Heading, TextInput} from '@primer/components'import { TaskContext } from './task-context'const TaskHeading = () =>{const {addTask,addTaskInput,tasks,setAddTaskInput,} = useContext(TaskContext)const onChange = e =>{setAddTaskInput(e.target.value)}const onSubmit = e =>{e.preventDefault()addTask(addTaskInput)}const displayableTasks = tasks.filter(task=> !task.archived)return (<React.Fragment><Heading ml={2}><CounterLabel mr={2}>{displayableTasks.length}</CounterLabel> Tasks</Heading><Box m={2}><form onSubmit={onSubmit}><TextInput value={addTaskInput} placeholder={'add new task'} onChange={onChange}/></form></Box></React.Fragment>)}export default TaskHeading
Now we need to write our TaskListBody component,
which will leave us with another component to
write: TaskListItem
import React, { useContext, Fragment } from 'react'import { Text } from '@primer/components'import { TaskContext } from './task-context'import TaskListItem from './tasklist-item'const TaskListBody = () => {const { displayableTasks } = useContext(TaskContext)console.log(displayableTasks)return (<Fragment>{displayableTasks.length !== 0 ? (displayableTasks.map(task => <TaskListItem key={task.id} task={task} />)) : (<Text p={2} m={3}>No Tasks Currently</Text>)}</Fragment>)}export default TaskListBody
Now we define our task list item, but because we want parts of the item to have unique behavior we will again need to define 2 new components when we finish:
Checkbox - so we dont have to rely on ugly html checkboxs
TaskText - to handle crossing out our text when we complete an item
import React, { useContext } from 'react'import { BorderBox, Flex, StyledOcticon } from '@primer/components'import { X as CloseIcon } from '@primer/octicons-react'import Checkbox from './task-checkbox'import TaskText from './task-text'import { TaskContext } from './task-context'export default ({ task }) => {const { complete, text, id: taskId } = taskconst { toggleComplete, toggleArchived } = useContext(TaskContext)return (<BorderBoxstyle={{ flexDirection: 'row' }}display={'flex'}p={2}mb={2}border={0}borderBottom={1}><Flex flexDirection={'row'} flexBasis={1}><Flex.Item flex={1} onClick={() => toggleComplete(taskId)}><Checkbox checked={complete} /></Flex.Item><Flex.Item flex={10} onClick={() => toggleComplete(taskId)}><TaskText completed={complete}>{text}</TaskText></Flex.Item><Flex.Item onClick={() => toggleArchived(taskId)} flex={0}><StyledOcticon icon={CloseIcon} size={15} color={'red.6'} /></Flex.Item></Flex></BorderBox>)}
Ok now for the last 2, which thankfully are very simple:
import React from 'react'import { BorderBox, StyledOcticon } from '@primer/components'import { Check } from '@primer/octicons-react'export default ({ checked }) => (<BorderBox textAlign={'center'} size={20} mr={2}>{checked ? (<StyledOcticoncolor={'green.6'}ml={1}verticalAlign={'text-top'}icon={Check}size={15}/>) : null}</BorderBox>)
And finally
import React from 'react'import styled, {css} from 'styled-components'import { Text } from '@primer/components'export default styled(Text)`${({completed})=> completed ? css`text-decoration: line-through` : ``};cursor: pointer;`// cool