user-avatar

Kyle J. Roux

jstacoder

Developer Program Member

a python/javascript/php/linux guru ready to take on the world...

CasePeer
orange, CA, USA
https://jstacoder.github.io

Hi
Home

Vanilla Redux 📝

3 min read

vanilla

Vanilla Redux

Why Just Redux?

Many people have told me they find it difficult to learn react-redux.
I think it is because even though they may know react, if they have not yet run into redux it can be very confusing.
So i found it useful to start out learning how to use redux by itself (without react).
After that when you need to connect your store to a react app you will have a much better idea of what is needed.

So what does that mean? We can make a small command line task list app as an example.

To start lets use npm or yarn to install our requirements, for this we will just use redux.

mkdir tasklist
cd tasklist
yarn init -y
yarn add redux uuid prompt

Once that finishes we can start by looking at what it means to use redux.

It calls itself

A predictable state container for javascript apps

What does that mean?

Well if you are familiar with react then you are familiar with using state, more traditionally inside of class based Components, but more recently also in functional Components using hooks (which yes, sort of make redux useless, but you want to learn it, maybe because you have to support code using it).

Basically it becomes the single source of truth in your app regarding state. If you need to check a value, you grab it from the redux store. If you want to update a value stored in the state, you pass an action to redux's dispatch function to kick off the update. It becomes very nice when you are working in a big project and you dont need to try to figure out how to update some value in the apps state, ie: which component handles it. You simply find or write the related action creator, and pass its return value to dispatch.

So lets look at some concrete examples, but first lets go over what we need to do:

Figure out what data we need to store

Choose what states we will need to support

Define action type constants to represent our state changes

Combine data and action types into helper functions aka: Action Creators

Write reducer functions to perform our state updates

Wire it into our task app

So now lets think about what we want our app to do.

A task list needs to be able to:

Add a new task with text, and incomplete state

List existing tasks

Set an incomplete task to complete

Delete a task

Now lets define what a task looks like

type Task {
id: number
text: String
complete: Bool
}

At some point we should add a due date but for now all we really need is an object with a text property and a property to store its state, ill just call it complete and default it to false.

First we need to import the stuff we want to use

const uuid = require('uuid')
const fs = require('fs')
const prompt = require('prompt')
const { createStore, combineReducers } = require('redux')

Now we will define our action types, in redux an action is an object, with a type and sometimes a payload of data.
An action creator is a function that returns an action.

// action type constants
const ADD_TASK = 'ADD_TASK'
const MARK_COMPLETE = 'MARK_COMPLETE'

Now we can write some functions to return our action objects

// action creator functions
const addTaskAction = ({ text }) => ({
type: ADD_TASK,
payload: {
text,
},
})
const markCompleteAction = taskId => ({
type: MARK_COMPLETE,
payload: {
taskId,
},
})

Now we need to write a reducer, which is a function that will take our current state, and an action, and uses it to update the state

const initialState = []
const taskListReducer = (
state = initialState,
{ type, payload: { text, taskId } = {} }
) => {
switch (type) {
case ADD_TASK:
return [
...state,
{
text,
complete: false,
id: uuid(),
},
]
case MARK_COMPLETE:
return state.map(task => {
if (task.id !== taskId) return task
return { ...task, complete: true }
})
default:
return state
}
}

Now for any of this to last after running, we need to have a way to save the data somehow, so we will just dump json to a file, and read from it to do that.

const loadInitialState = () => {
return new Promise((resolve, reject) => {
return fs.readFile('./tasks.json', (err, result) => {
if (err) {
reject(err)
}
let tasks
try {
tasks = JSON.parse(result.toString())
} catch (e) {
tasks = []
}
resolve({ tasks })
})
})
}
const saveState = ({ tasks }) => {
const data = JSON.stringify(tasks)
try {
JSON.parse(data)
fs.writeFile('./tasks.json', data, (err, res) => {})
} catch (e) {
console.log(err)
}
}

Now to tie redux up and have it ready we need to create our "store" using the aptly named redux function createStore, which takes 3 arguments:

Your root reducer (the combination of any reducers you have)

Any needed initial state

An "enhancer" which i wont use or go into further here.

Since we want to load our tasks from a file, we need to tie that into creating our store:

const loadStore = () => {
return loadInitialState().then(initialState => {
return createStore(
combineReducers({ tasks: taskListReducer }),
initialState
)
})
}

Now that we have a store, that gives us the dispatch function we can use to execute our actions, so lets write some helper functions to do that for us.

We need functions to:

Add a task

Mark a task complete

Display tasks

const addTask = ({ dispatch }, text) => {
dispatch(addTaskAction({ text }))
}
const markComplete = ({ dispatch }, taskId) => {
dispatch(markCompleteAction(taskId))
}
const displayTasks = tasks => {
tasks.forEach(({ text, complete }, index) => {
console.log(`${index + 1} [${complete ? 'x' : ' '}] ${text}`)
})
}

To get our user input, we will use the prompt libtrary. which takes an object config, namely name which is how the data is returned, and description which is the question to ask the user.

const askWhatToDo = [
{
name: 'whatToDo',
description:
'what would you like to do?\n[A] Add task\n[L] List tasks\n[C] Complete Task',
},
]
const askWhatTaskToAdd = [
{
name: 'text',
description: 'What should the text of the task be?',
},
]
const askWhatTaskToComplete = [
{
name: 'taskIndex',
description: 'What task Should we mark as complete?',
},
]

Now lets tie the questions to our functions

When they want to add a task we need to ask what the task is

const doAddTask = store => {
prompt.get(askWhatTaskToAdd, (err, { text }) => {
console.log(`adding ${text}`)
addTask(store, text)
})
}

To complete a task we need to know which one to mark complete

const doCompleteTask = store => {
prompt.get(askWhatTaskToComplete, (err, { taskIndex }) => {
const { tasks } = store.getState()
const { id, text } = tasks[taskIndex]
console.log(`completing ${text}`)
markComplete(store, id)
})
}

Listing the tasks is easy, no questions needed

const doListTasks = store => {
const { tasks } = store.getState()
displayTasks(tasks)
}

Now lets tie it all together along with a question to find out what the user wants to do.

loadStore().then(store => {
store.subscribe(() => {
const state = store.getState()
saveState(state)
})
prompt.get(askWhatToDo, (err, { whatToDo }) => {
const actions = {
a: doAddTask,
l: doListTasks,
c: doCompleteTask,
}
console.log(`you chose ${whatToDo}`)
actions[whatToDo.toLowerCase()](store)
})
})
Comments
Home