DEV Community

Nikhil Kumaran S
Nikhil Kumaran S

Posted on • Edited on • Originally published at nikhilkumaran.vercel.app

You probably don't need Redux: Use React Context + useReducer hook

I would like to amend this: don't use Redux until you have problems with vanilla React. - Dan Abramov

Dan said this way back in 2016, and now that we have React Context and useReducer hook, the use cases of redux is very minimal. In this post, we will create a good old todo list example using Context and useReducer hook.

First, let's set up our initial state and actions. Let our todo app have three actions - add, remove, and toggle completed.

const initialState = {
  todoList: []
};

const actions = {
  ADD_TODO_ITEM: "ADD_TODO_ITEM",
  REMOVE_TODO_ITEM: "REMOVE_TODO_ITEM",
  TOGGLE_COMPLETED: "TOGGLE_COMPLETED"
};
Enter fullscreen mode Exit fullscreen mode

Now let's add a reducer function to handle our actions.

const reducer = (state, action) => {
  switch (action.type) {
    case actions.ADD_TODO_ITEM:
      return {
        todoList: [
          ...state.todoList,
          {
            id: new Date().valueOf(),
            label: action.todoItemLabel,
            completed: false
          }
        ]
      };
    case actions.REMOVE_TODO_ITEM: {
      const filteredTodoItem = state.todoList.filter(
        (todoItem) => todoItem.id !== action.todoItemId
      );
      return { todoList: filteredTodoItem };
    }
    case actions.TOGGLE_COMPLETED: {
      const updatedTodoList = state.todoList.map((todoItem) =>
        todoItem.id === action.todoItemId
          ? { ...todoItem, completed: !todoItem.completed }
          : todoItem
      );
      return { todoList: updatedTodoList };
    }
    default:
      return state;
  }
};
Enter fullscreen mode Exit fullscreen mode

Let's break it down.

  • In the ADD_TODO_ITEM action, I'm spreading the existing list and adding a new todo item to the list with id(unique-ish), label(user-entered value), and completed flag.
  • In the REMOVE_TODO_ITEM action, I'm filtering out the todo item that needs to be removed based on the id.
  • In the TOGGLE_COMPLETED action, I'm looping through all the todo items and toggling the completed flag based on the id.

Now, let's wire these up with Context and useReducer. Let's create a TodoListContext.

const TodoListContext = React.createContext();
Enter fullscreen mode Exit fullscreen mode

Let's create a Provider function that returns our TodoListContext's Provider.

const Provider = ({ children }) => {
  const [state, dispatch] = React.useReducer(reducer, initialState);

  const value = {
    todoList: state.todoList,
    addTodoItem: (todoItemLabel) => {
      dispatch({ type: actions.ADD_TODO_ITEM, todoItemLabel });
    },
    removeTodoItem: (todoItemId) => {
      dispatch({ type: actions.REMOVE_TODO_ITEM, todoItemId });
    },
    markAsCompleted: (todoItemId) => {
      dispatch({ type: actions.TOGGLE_COMPLETED, todoItemId });
    }
  };

  return (
    <TodoListContext.Provider value={value}>
      {children}
    </TodoListContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

Let's break it down.

  • We are passing our reducer function and our initialState to the useReducer hook. This will return state and dispatch. The state will have our initialState and the dispatch is used to trigger our actions, just like in redux.
  • In the value object, we have todoList state, and three functions addTodoItem, removeTodoItem, and markAsCompleted which trigger ADD_TODO_ITEM, REMOVE_TODO_ITEM, and TOGGLE_COMPLETED actions respectively.
  • We are passing our value object as prop to the TodoListContext's Provider, so that we can access it using useContext.

Great, now our global store and reducers are setup. Let's now create two components AddTodo and TodoList which will consume our store.

const AddTodo = () => {
  const [inputValue, setInputValue] = React.useState("");
  const { addTodoItem } = React.useContext(TodoListContext);

  return (
    <>
      <input
        type="text"
        value={inputValue}
        placeholder={"Type and add todo item"}
        onChange={(e) => setInputValue(e.target.value)}
      />
      <button
        onClick={() => {
          addTodoItem(inputValue);
          setInputValue("");
        }}
      >
        Add
      </button>
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

In AddTodo, we are using useContext to subscribe to our TodoListContext and getting addTodoItem dispatch function. This component has an input field where the user enters the todo item and an add button to add the todo item to the list.

const TodoList = () => {
  const { todoList, removeTodoItem, markAsCompleted } = React.useContext(
    TodoListContext
  );
  return (
    <ul>
      {todoList.map((todoItem) => (
        <li
          className={todoItem.completed ? "completed" : ""}
          key={todoItem.id}
          onClick={() => markAsCompleted(todoItem.id)}
        >
          {todoItem.label}
          <button
            className="delete"
            onClick={() => removeTodoItem(todoItem.id)}
          >
            X
          </button>
        </li>
      ))}
    </ul>
  );
};
Enter fullscreen mode Exit fullscreen mode

In TodoList component, we are using useContext to subscribe to our TodoListContext and getting todoList state, removeTodoItem, and andmarkAsCompleted dispatch functions. We are mapping through the todoList and rendering the todo items and a remove(X) button next to it. On clicking on an item we are marking it as complete and when clicking on X button we are removing it from the list.

Finally, let's wrap our two components with our Provider.

export default function App() {
  return (
    <Provider>
      <AddTodo />
      <TodoList />
    </Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Great. We used Context and useReducer hook to manage our state as an alternative to redux. You can check the working code in codesandbox.

That's it, folks, Thanks for reading this blog post. Hope it's been useful for you. Please do comment your questions and suggestions.

Top comments (23)

Collapse
 
artydev profile image
artydev • Edited

Hello, look at the Meiosis pattern.
Regards
Search Meiosis on Dev to

Collapse
 
leob profile image
leob • Edited

First time I hear about it: dev.to/artydev/frontend-state-mana...

The explanations are terse, but yes I saw this:

meiosis.js.org/

"Think Redux, MobX, Cerebral, React Context, etc. but without having library imports everywhere and being tied to a framework API. Instead, just plain functions and objects. All you need is a simple stream library such as Flyd or Mithril Stream"

So you don't need to have library imports ... but you do need a library ;)

Collapse
 
artydev profile image
artydev • Edited

No :-)
All you need is two streams functions map and scan that you can implement yourself or use the simple implementation mithril-stream

And the function I use to merge states is a thirty lines of code called mergerino, which extends Object.assign.

I would not call them library, they are so tiny and very specialised

You can use whathever you want for the view (event document.innerHTML)

Here I use JQuery
MeiosisJS

Look at this for a starting pont :
observables

Regards

code for mergerino

const assign = Object.assign || ((a, b) => (b && Object.keys(b).forEach(k => (a[k] = b[k])), a))

const run = (isArr, copy, patch) => {
  const type = typeof patch
  if (patch && type === 'object') {
    if (Array.isArray(patch)) for (const p of patch) copy = run(isArr, copy, p)
    else {
      for (const k of Object.keys(patch)) {
        const val = patch[k]
        if (typeof val === 'function') copy[k] = val(copy[k], merge)
        else if (val === undefined) isArr && !isNaN(k) ? copy.splice(k, 1) : delete copy[k]
        else if (val === null || typeof val !== 'object' || Array.isArray(val)) copy[k] = val
        else if (typeof copy[k] === 'object') copy[k] = val === copy[k] ? val : merge(copy[k], val)
        else copy[k] = run(false, {}, val)
      }
    }
  } else if (type === 'function') copy = patch(copy, merge)
  return copy
}

const merge = (source, ...patches) => {
  const isArr = Array.isArray(source)
  return run(isArr, isArr ? source.slice() : assign({}, source), patches)
}

export default merge

the code of mithril-stream is here from Isiah Meadows Mithril's maintener.

/* eslint-disable */
;(function() {
"use strict"
/* eslint-enable */
Stream.SKIP = {}
Stream.lift = lift
Stream.scan = scan
Stream.merge = merge
Stream.combine = combine
Stream.scanMerge = scanMerge
Stream["fantasy-land/of"] = Stream

var warnedHalt = false
Object.defineProperty(Stream, "HALT", {
    get: function() {
        warnedHalt || console.log("HALT is deprecated and has been renamed to SKIP");
        warnedHalt = true
        return Stream.SKIP
    }
})

function Stream(value) {
    var dependentStreams = []
    var dependentFns = []

    function stream(v) {
        if (arguments.length && v !== Stream.SKIP) {
            value = v
            if (open(stream)) {
                stream._changing()
                stream._state = "active"
                dependentStreams.forEach(function(s, i) { s(dependentFns[i](value)) })
            }
        }

        return value
    }

    stream.constructor = Stream
    stream._state = arguments.length && value !== Stream.SKIP ? "active" : "pending"
    stream._parents = []

    stream._changing = function() {
        if (open(stream)) stream._state = "changing"
        dependentStreams.forEach(function(s) {
            s._changing()
        })
    }

    stream._map = function(fn, ignoreInitial) {
        var target = ignoreInitial ? Stream() : Stream(fn(value))
        target._parents.push(stream)
        dependentStreams.push(target)
        dependentFns.push(fn)
        return target
    }

    stream.map = function(fn) {
        return stream._map(fn, stream._state !== "active")
    }

    var end
    function createEnd() {
        end = Stream()
        end.map(function(value) {
            if (value === true) {
                stream._parents.forEach(function (p) {p._unregisterChild(stream)})
                stream._state = "ended"
                stream._parents.length = dependentStreams.length = dependentFns.length = 0
            }
            return value
        })
        return end
    }

    stream.toJSON = function() { return value != null && typeof value.toJSON === "function" ? value.toJSON() : value }

    stream["fantasy-land/map"] = stream.map
    stream["fantasy-land/ap"] = function(x) { return combine(function(s1, s2) { return s1()(s2()) }, [x, stream]) }

    stream._unregisterChild = function(child) {
        var childIndex = dependentStreams.indexOf(child)
        if (childIndex !== -1) {
            dependentStreams.splice(childIndex, 1)
            dependentFns.splice(childIndex, 1)
        }
    }

    Object.defineProperty(stream, "end", {
        get: function() { return end || createEnd() }
    })

    return stream
}

function combine(fn, streams) {
    var ready = streams.every(function(s) {
        if (s.constructor !== Stream)
            throw new Error("Ensure that each item passed to stream.combine/stream.merge/lift is a stream")
        return s._state === "active"
    })
    var stream = ready
        ? Stream(fn.apply(null, streams.concat([streams])))
        : Stream()

    var changed = []

    var mappers = streams.map(function(s) {
        return s._map(function(value) {
            changed.push(s)
            if (ready || streams.every(function(s) { return s._state !== "pending" })) {
                ready = true
                stream(fn.apply(null, streams.concat([changed])))
                changed = []
            }
            return value
        }, true)
    })

    var endStream = stream.end.map(function(value) {
        if (value === true) {
            mappers.forEach(function(mapper) { mapper.end(true) })
            endStream.end(true)
        }
        return undefined
    })

    return stream
}

function merge(streams) {
    return combine(function() { return streams.map(function(s) { return s() }) }, streams)
}

function scan(fn, acc, origin) {
    var stream = origin.map(function(v) {
        var next = fn(acc, v)
        if (next !== Stream.SKIP) acc = next
        return next
    })
    stream(acc)
    return stream
}

function scanMerge(tuples, seed) {
    var streams = tuples.map(function(tuple) { return tuple[0] })

    var stream = combine(function() {
        var changed = arguments[arguments.length - 1]
        streams.forEach(function(stream, i) {
            if (changed.indexOf(stream) > -1)
                seed = tuples[i][1](seed, stream())
        })

        return seed
    }, streams)

    stream(seed)

    return stream
}

function lift() {
    var fn = arguments[0]
    var streams = Array.prototype.slice.call(arguments, 1)
    return merge(streams).map(function(streams) {
        return fn.apply(undefined, streams)
    })
}

function open(s) {
    return s._state === "pending" || s._state === "active" || s._state === "changing"
}

if (typeof module !== "undefined") module["exports"] = Stream
else if (typeof window.m === "function" && !("stream" in window.m)) window.m.stream = Stream
else window.m = {stream : Stream}

}());
Thread Thread
 
leob profile image
leob

So you need to learn how think in terms of streams and observables (I think RxJS is a famous library in this area) ... if you're not familiar with that paradigm yet then it's definitely a learning curve compared to the way you'd normally do things with Redux or Context.

Thread Thread
 
artydev profile image
artydev • Edited

Think of streams as Excel cells and you will have a good starting point.
I have written a few lines as introduction here

streams

Thread Thread
 
artydev profile image
artydev • Edited

You absolutely don't need RxJS, as I said earlier you only need map and scan for Meiosis

Here is very simple stream implementaion with a map function :

function Stream (value) {
  let storedvalue = value
  let mappers = []
  function stream (newvalue) {
    if (arguments.length) {
      mappers.map(f => f(newvalue))
      storedvalue = newvalue
    }
    return storedvalue
  }
  stream.map = function (f) {
    mappers.push(f)
  }
  return stream
}

s = Stream()

document.addEventListener("mousemove", s)

s.map(t => divmouse.innerHTML = (`<h2>(${t.clientX}, ${t.clientY})</h2>`)) 

You can test it here :

StreamMap

Regards

Thread Thread
 
leob profile image
leob

Thanks, excellent, yes I get the idea ... I only mentioned RxJS because that's how I ever heard about streams and observables :-)

So can I put it like this: Streams provide a different approach (paradigm, if you will) to working with state in an "immutable" way?

Because that's basically what Redux and Context (and all of React, for that matter) tell us:

Do NOT modify state directly - return a new copy of your state, so treat it as 'immutable' ... streams do just that, but in a different (and probably easier or more powerful) way.

Thread Thread
 
artydev profile image
artydev • Edited

:-)

I am in favor of simplicity, If you are creating a project for your own needs, you really don't need heavy tools.

Use Meiosis with whatever view library you want or nothing eventually.

May I suggest you to use Mithril ?, simple, light and a very kind community :-)

Regards

Thread Thread
 
leob profile image
leob

I've heard about Mithril, but I also heard that Svelte is the greatest thing since sliced bread ... can you compare those two?

Thread Thread
 
artydev profile image
artydev • Edited

Much has been said on Svelte the "javascript compiler".
I can only agree with the speed and tiny apps it produces.
But for my needs I don't see what it brings more than Mithril

Mithril is only Javascript, with Svelte you have to learn a new syntax,
and you can't start without tooling, so integrate it into existing applications is not obvious

For example I needed to extends functionality to an Editor by adding a 'photo' button
with Mithril it was a piece of cake, It would not been that easier with Svelte.

JoditMithril

The more you use Mithril the more you know Javascript, I am not sure this is the case for all the well known frameworks

Thread Thread
 
leob profile image
leob

Right, yes I understand that they're different philosophies, more or less the opposite ...

Mithril is something you can drop into an existing page to enhance it, Svelte is more something you use to build a complete standalone app, it doesn't easily integrate, and it introduces its own tooling and syntax (a bit comparable with Vue with its template syntax, while React takes pride in sticking closer to standard JS, at least that's what it claims).

Totally different beasts really :-)

Thread Thread
 
artydev profile image
artydev • Edited

:-) you can also build complete apps with Mithril.

Among Mithril's users Nike.

Look carefully to this page :-)

Who use Mithril

Regards

Collapse
 
nikhilkumaran profile image
Nikhil Kumaran S

Yeah. Looks similar.

Collapse
 
leob profile image
leob • Edited

Good explanation, but I can't help but notice how similar the code looks compared to what you would write if you use Redux - you still write reducers, actions, dispatch, etcetera ... so, I could be wrong, but at first sight we don't seem to have gained much in terms of reducing boilerplate.

The only simplification then seems to be that we can omit one or two dependencies from our package.json (but Redux doesn't add that much to the bundle size, it's a pretty small library).

Note that I'm not saying that this approach (Context) is worse than using Redux, but I also don't see immediately how it's better or simpler - the way you manage state with Context is largely the same.

Or am I missing something?

Collapse
 
nikhilkumaran profile image
Nikhil Kumaran S

You are right. I never considered "reducing boilerplate" as a goal. I wanted to show that we can achieve redux like functionality in vanilla react itself.

Collapse
 
leob profile image
leob

Understood ... but look at this one - we might not need Redux, but we might even also not need Context! dev.to/g_abud/why-i-quit-redux-1knl

Thread Thread
 
nikhilkumaran profile image
Nikhil Kumaran S

Sure will check it out✌🏽

Collapse
 
sylvestertarnowski profile image
Sylvester

Really nice introduction to Context. Is there a well-established way to use asynchronous actions, similar to redux thunk?

Collapse
 
nikhilkumaran profile image
Nikhil Kumaran S

You can declare an async function in the value prop to the Provider. Here's an example: codesandbox

P.S. Not sure if this exactly what you asked for.

Collapse
 
sylvestertarnowski profile image
Sylvester

Yes, this is it. It occurred to me when I saw the example that I don't need middleware to access state - here I can simply refer to it if I need for example some intersection between fetched data and current state.

Thank you :-)

Thread Thread
 
nikhilkumaran profile image
Nikhil Kumaran S

You're welcomeπŸ‘πŸ½

Collapse
 
leomjaques profile image
leo πŸ‘¨πŸ»β€πŸ’»

It was! Thanks mate :)

Collapse
 
nikhilkumaran profile image
Nikhil Kumaran S • Edited

Glad you liked it.