This is a port of the popular library redux to pyhton.
This library is a pythonic implementation of the original javascript-implementation with full-api support.
This package is currently compatible with python2.7 and python >= 3.5. (At least tested for these versions)
- pyrsistent: persistent immutable data-structure
- singledispatch: for python2 its a backport, for python3 its build-in
For a very good and detailed description how to use redux, please refer to the javascript-documentation
Here i will describe the api and more importantly the changes I made comparing to the javascript-api
To create a Store just call the create_store
function:
from pyredux import create_store
store = create_store(reducer)
This will give you a fully initialized store-object to work with. This method is nothing more than a simple factory- / builder-method to initialize a store.
The Store-api is a little different to the js-api:
1. To retrieve the actual state instead of a getter a property is used: *actual_state = store.state*
2. unsubscribe is done via store-method
The state of the store is safed in an immutable pmap
Therefor you can not mutably change it in the reducer and you will
always have to return state.update({...})
from your reducer. The second constraint
is that you will always have to deal with an immutable dict as the current-state, even though
you would have just needed a list or a variable in your current reducer!
On the other hand you don't have to worry about immutability your self.
Keep in mind that although the state-object it self is immutable, you can
store mutable data in it. This will lead to unwanted behaviour as i will
only call the listeners when old_state is not new_state
. So if you
will only mutably change your data, than your listeners will never be called.
For further information how to use pyrsitent please refer to the documentation and the examples in their examples-section
A reducer is named after the function that gets called from the reduce
function.
One of the key-concepts of redux is, that the current state gets computed in the
following manner:
new_state = reduce(reducer_func, [initial_state, action1, action2, ..., actionN])
For performance reasons the old state is saved and every time a new Action is dispatched only the one function-call is necessary to calculate the new state. In pseudo-code:
state [N] = reduce(reducer_func, [state[N-1], action])
Because the state is safed it's essential that updates to the state will be applied in an immutable manner.
Although it's more or less a standard state-reducer-pattern the signature of the reducer for this library is changed! To write your own reducer-function the following signature has to provided:
def my_custom_reducer(action, state=pmap({}):
return state
Whereas the "normal" reducer signature would be: reducer(state, action)
The default-value of state
is the initial-state of your reducer, which has
to be provided by any reducer.
This design-choice was made for two reasons:
A more pythonic function - signature, as it is not allowed in pyhton to have positional arguments after named arguments. So with the traditional signature you would have to write something like this:
# NOT the function-signature to use with pyredux!!!!
def my_custom_reducer(state=pmap({}), action=None):
return state
This does not look nice, and theoratically you would have to handle
the case where an action was not provided (maybe be some middleware) and it is
actually None
.
Because of the lack of a case-statement or an comprehensive pattern-matching functionality,
writing a reducer that handles more than one action would result in a long
chained if-elif-else
statement. Instead of this, the python-functionality of
singledispatch can be used.
Singledispatch works only on the first argument of a function, but results in
a very cool / clean / pythonic way of writing reducers for different actions:
from pyredux import default_reducer
@default_reducer
def my_reducer(action, state=pmap({"static": True})):
return state
@my_reducer.register(ActionA)
def _(action, state):
print("received action of type 'ActionA'")
return state
The decorator @default_reducer
is just another name for @singledispatch
By using this facility your default case (actions your reducer does not care about)
can always look like the one provided in this example (just return the state). For every
action not registered before, the function decorated with @default_reducer
will
automatically been called.
Every action action you want to process with this reducer, must be registered and can now be processed in a function strictly responsible for that action-type. Even more actions (which are namedtuples, more on this later in this guide) can have "subtypes":
@default_reducer
def my_reducer(action, state=pmap({"static": True})):
return state
@my_reducer.register(ActionA)
def _(action, state):
print("received action of type 'ActionA'")
return state
@my_reducer.register(FancyAction)
def _(action, state):
if action.type == "fancy_1":
print("Received action of type 'FancyAction' with a fanciness of 1")
elif action.type == "fancy_2":
print("Received an even more fancy action")
else:
print("Too fancy for me")
return state
This way the action-handling becomes a less error-pone task and you get
a way of processing actions with a "subtype" without having to care
about nested long if-elif-else
structures
As your application will grow there is a point where you don't want to
handle all actions in one reducer. So it's time to split them up into several
reducers and provide a combined reducer to the store.
To combine reducers the function combine_reducer
can be used.
def a_reducer(action, state):
return state
def b_reducer(action, state):
return state
store = create_store(combine_reducer([a_reducer, b_reducer]))
Note than when splitting up your reducers into many functions your code becomes better structured and more readable, but each reducer can now only access it's own "subtree" of the state!
Now your state will roughly have this shape:
print(store.state)
>>> {"a_reducer": ..., "b_reducer": ...}
If you provide your functions via list or tuple to the function combine_reducer
,
your "state-dict" will have the function-names as keys.
Instead of splitting-up the state tree by the function-names
you can use a dict that provides the key for the state and the
corresponding function:
store = create_store(combine_reducer(
{"a": a_reducer, "b": b_reducer})
)
assert store.state == pmap({"a": ..., "b": ...})
For a combined reducer, every "subreducer" will receive every action dispatched, but only their "own" state-subtree.
The function create_action_type
creates a namedtuple with with the attributes:
type
and payload
. During creation, the type
attribute
will get the name of the class as default value and the default value of
payload
will be None
. When you instantiate
your custom-action-type you can override both attributes, whereas the
type of the class will stay the same.
As a convenience-function create_typed_action_creator
is provided.
The function returns the action-type for registering at the reducer
and a builder-function which can be used in the following way:
ActionBaseType, basic_action_creator = create_typed_action_creator("ActionA")
a_action = basic_action_creator()
c_action = basic_action_creator("payload_c", "myCustomSubType")
assert type(a_action) == type(c_action)
assert c_action.type == "myCustomSubType" and c_action.payload == "payload_c"
By that ,the application is not bound to a certain type, but has just to handle a function call. That should increase loose coupling.
Actions can be dispatched through the store and are transmitted
to the (combined-)reducer. To make the dispatch mechanism work
actions can't just be a dictionary. Therefor namedtuples are used.
To create your own Action-type the function create_action_type
can be used:
CustomActionType = create_action_type("Custom")
action = CustomActionType()
assert action.type == "Custom"
assert isinstance(action, CustomActionType)
The argument of create_action_type
determines the type
of the named tuple:
CustomActionType = create_action_type("Custom")
str(CustomActionType)
>>> "<class 'pyredux.Actions.Custom'>"
str(CustomActionType().type)
>>> "Custom"
If during the instantiation of CustomActionType
no subtype
is provided than the attribute type
will be set to be the same as
the class:
CustomActionType = create_action_type("Custom")
str(CustomActionType)
>>> "<class 'pyredux.Actions.Custom'>"
action = CustomActionType(type="something_different")
action.type
>>> "something_different"
type(action)
>>> "<class 'pyredux.Actions.Custom'>"
An action can have a payload:
CustomActionType = create_action_type("Custom")
action = CustomActionType("my super payload")
print(action.payload)
>>> "my super payload"
Normally you wont tie your application to certain Action-types,
because it would couple your business-logic to a certain signal.
That decreases portability. Instead you can use the function:
create_typed_action_creator
to create a ActionType and
a corresponding creator-function.
CustomActionType, creator_func = create_typed_action_creator("Siganl")
@default_reducer()
def signal(action, state=pmap({})):
return state
@signal.register(CustomActionType)
def _(action, state):
... # Do somthing
return new_state
class Application(object):
def do_something(self):
action = creator_func("my payload", "my_sub_type")
store.dispatch(action)
...
This is similar to the Factory-Pattern. As another example here how you
can compose the creator function with the store.dispatch
function to create "send my action right away function".
from pyredux.utils import compose
CustomActionType, creator_func = create_typed_action_creator("Signal")
send_my_stuff = compose(store.dispatch, creator_func)
class Application(object):
def on_click():
send_my_stuff("was clicked")
As you should not do anything with side-effects from inside your reducer,
a good place encapsulate all these behaviour of your application is in
the middleware. A custom middleware can be applied via apply_middleware
:
from pyredux import create_store, apply_middleware
store = create_store(
reducer,
enhancer=apply_middleware(middleware_b, middleware_a)
)
This function-call will automatically wrap the the store.dispatch
function,
so that every action travels through the middleware-chain before
reaching your reducer. A middleware-function has the following signature:
def middleware_a(store):
def _next_wrapper(next_middleware):
def _middleware(action):
... # Do something with or wihtout side-effects here
return next_middleware(action)
return _middleware
return _next_wrapper
or as a short-hand with nicer syntax:
from pyredux import middleware
@middleware
def middleware_decorated(store, next_middleware, action):
... # Do somthing here
return next_middleware(action)
For more detailed information about the redux-middleware-concept please refer to the reduxjs-documentation