The official website calls Redux "a predictable state container for JS Apps". The main benefit of Redux is that it shines a light on your global application state, allowing you to trace when, where, why, and how your application's state changed.

Mutable global state is indeed bad, mmmkay

The idea that global state is bad is drilled into programmers since the beginning of their programming career. One thing that isn't clearly explained however is why.

There are several issues with global state, but the most important problem is that not only can anyone change the application state, but also that there's no way you can tell who changed the state.

If your first contact with Redux comes in the context of a React Redux application, the Redux side of things will feel like magic.

As I usually try to understand the tools I'm using, the following tries to be a toy implementation of Redux in PHP, in the hopes of gaining a deeper understanding of the concepts behind Redux.

The most important realization was that Redux is mostly convention, with a bit of library code to tie it all together. As you could probably tell from the title, the language we're gonna use is PHP, so consider yourself warned.

Great. Let's go ahead and start with the simple state we're going to manage:

$initialState = [ 'count' => [ 'count' => 1 ], ];

Simple enough. As per the Redux convention, we don't modify the state directly:

$initialState['count']['count'] +=1

But we can interact with the state using actions:

const INCREMENT_ACTION = 'INCREMENT'; const DECREMENT_ACTION = 'DECREMENT'; $actions = [ 'increment' => [ 'type' => INCREMENT_ACTION ], 'decrement' => [ 'type' => DECREMENT_ACTION ], ];

The state and actions are plain PHP arrays, no magic about them.

We also need a reducer, a function that takes our state, processes the action and generates the new state:

function countReducer(array $state, $action) { switch($action['type']) { case INCREMENT_ACTION: return array_replace([], $state, ['count' => $state['count'] + 1]); case DECREMENT_ACTION: return array_replace([], $state, ['count' => $state['count'] - 1]); default: return $state; } }

We're just generating a new state array by incrementing / decrementing the previous value. Reducers receive part of the initial state array, based on their key, but more on that later when we're going to introduce combineReducers . For now, let's imagine countReducer receives the value in $initialState['count'] :

[ 'count' => 1 ]

We still haven't written any kind of library code yet, so far it's all been convention.

If we take a look at the Redux API, we start by using the createStore method. We can then use the dispatch and subscribe methods to dispatch actions and subscribe to state change updates.

Translating that to PHP, let's create a Store class. How would that look:

class Store { protected array $state; protected Closure $reducer; public function __construct(callable $reducer, array $initialState) { $this->state = $initialState; $this->reducer = Closure::fromCallable($reducer); } }

The class now accepts an initial state and a reducer function. In Redux you can combine multiple reducers into one and pass that combined result to the createStore method, later on we'll discuss how we can achieve that.

Great, how about dispatch and subscribe? Let's start by adding a method to fetch the store's state and then the subscribe method:

protected array $listeners = []; public function getState() { return $this->state; } public function subscribe(callable $listener) { $this->listeners[] = Closure::fromCallable($listener); }

Subscribing to the store means adding a callback function which will be called when the state updates. How would we use the subscribe method?

$store->subscribe(function($state) { print_r($state); });

How about dispatch:

public function dispatch(array $action) { $this->state = ($this->reducer)($this->getState(), $action); foreach($this->listeners as $listener) { $listener($this->getState()); } }

In order to dispatch an action, we need to generate the new state by calling the reducer and then notifying all the subscribed listeners.

Awesome. Do we need anything else? No, that's pretty much it. We now have a basic Redux implementation in PHP. Here's how you'd use this:

<?php class Store { protected array $state; protected Closure $reducer; protected array $listeners = []; public function __construct( callable $reducer, array $initialState) { $this->state = $initialState; $this->reducer = Closure::fromCallable($reducer); } public function getState() { return $this->state; } public function subscribe(callable $listener) { $this->listeners[] = Closure::fromCallable($listener); } public function dispatch(array $action) { $this->state = ($this->reducer)($this->getState(), $action); foreach($this->listeners as $listener) { $listener($this->getState()); } } } $initialState = [ 'count' => [ 'count' => 1 ], ]; const INCREMENT_ACTION = 'INCREMENT'; const DECREMENT_ACTION = 'DECREMENT'; $actions = [ 'increment' => [ 'type' => INCREMENT_ACTION ], 'decrement' => [ 'type' => DECREMENT_ACTION ], ]; function countReducer(array $state, $action) { switch($action['type']) { case INCREMENT_ACTION: return array_replace([], $state, ['count' => $state['count'] + 1]); case DECREMENT_ACTION: return array_replace([], $state, ['count' => $state['count'] - 1]); default: return $state; } } $store = new Store('countReducer', $initialState['count']); $store->subscribe(function($state) { print_r($state); }); $store->dispatch($actions['increment']); $store->dispatch($actions['increment']); $store->dispatch($actions['increment']); $store->dispatch($actions['decrement']);

The output of the above code is:

Array ( [count] => 2 ) Array ( [count] => 3 ) Array ( [count] => 4 ) Array ( [count] => 3 )

You can then add custom data on the action and modify the reducer to take the custom data into account:

const INCREMENT_BY_ACTION = 'INCREMENT_BY'; function countReducer(array $state, $action) { switch($action['type']) { case INCREMENT_BY_ACTION; $value = $action['value'] ?? 0; return array_replace([], $state, ['count' => $state['count'] + $value]); case INCREMENT_ACTION: return array_replace([], $state, ['count' => $state['count'] + 1]); case DECREMENT_ACTION: return array_replace([], $state, ['count' => $state['count'] - 1]); default: return $state; } } $store->dispatch(['type' => INCREMENT_BY_ACTION, 'value' => 5]);

Awesome. The only problem now is that our store accepts only one reducer. That's also the case for the createStore Redux function. The way they solve this problem is by providing a combineReducers method:

rootReducer = combineReducers({potato: potatoReducer, tomato: tomatoReducer});

So the method takes in a list of reducers and ends up providing one reducer that combines them. How would that look in PHP?

function combineReducers(array $reducers) { return function(array $state, $action) use ($reducers) { $newState = $state; foreach($reducers as $stateKey => $reducer) { if(!isset($newState[$stateKey])) { $newState[$stateKey] = []; } $newState[$stateKey] = $reducer($newState[$stateKey], $action); } return $newState; }; }

We start by accepting an array of reducers and we need to produce a callback that sticks to the reducer signature (takes in state and an action, and produces the resulting state).

Inside that callback we then go through all of the registered reducers, call them one by one with their appropriate piece of the state (based on the reducer key) and return the resulting state.

We would then call this as follows:

$initialState = [ 'count' => [ 'count' => 1 ], 'sum' => 0 ]; function sumReducer($state, $action) { switch($action['type']) { case ADD_SUM: return $state + 1; default: return $state; } $store = new Store(combineReducers([ 'count' => 'countReducer', 'sum' => 'sumReducer' ]), $initialState);

We can now add multiple reducers to the store, to be able to split up the task of generating new state.

We've now wrapped up our toy implementation of Redux. I hope the article above, even if written in PHP, helped shed some light on how Redux works behind the scenes.

Follow me on Twitter @sdnunca for more content.