Implement your own Redux in Java. Part 1: Basic Redux functionality
javareduxTo learn and understand new tech things I'm often implementing them on my own. I mean, How hard can it be? This time I'd like to show how to implement Redux with Java targeted towards JavaFX. The purpose here is not to actually use this implementation because there is already a really good one available. The purpose is only to see that Redux actually isn't that complex when it comes to a basic implementation of the library. As this topic includes a lot of sub-topics I will start a little series of blog posts that will cover the implementation of a Redux library in Java, the extension mechanisms of Redux and how to use them with JavaFX.
Topics in this blog series (maybe I will adjust this in the process of writing):
- Part 1: Basic Redux functionality
- Part 2: Simple FXML Views
- Part 3: First approach on Middlewares
- Part 4: Improved Middleware API
- Part 5: Async behavior with Middlewares
- Part 6: Other examples for Middlewares
- Store-enhancers
- Dev-Tools with time-travel-debugging
- advanced FXML views
- declarative JavaFX Views
- ReduxFX - an actual library
The first thing when implementing a library is to think about how a user would like to use it. How do you like your app code to look like?
First let's create some actions. I'm using the classic Todo app as an example here so we need an "AddItemAction":
public class AddItemAction implements Action {
private final String value;
public AddItemAction(String value) {
this.value = value;
}
public String getValue() {
return value;
}
// equals & hashcode
}
To give the class a little more context and to make the implementation of reducers easier I've also created a marker
interface Action
here that doesn't contain any methods.
The next step is to create a class for the State. This could look like this:
public class TodoState {
private final List<String> items;
public TodoState(List<String> items) {
this.items = items;
}
public List<String> getItems() {
return items;
}
public TodoState withItems(List<String> items) {
if(items.equals(this.items)) {
return this;
} else {
return new TodoState(items);
}
}
}
Here I'm implementing the state as an immutable class. This is important for Redux: You can't change the fields of the state object. Instead you have to create a copy that has your desired changes applied. In this example I'm implementing the immutability by hand. Of cource this won't scale for bigger applications. So for real-world applications I would suggest you to use a library like Immutables.org that creates immutable classes for you.
The next step for a Redux application is the Reducer. This can also be implemented with pure Java code like this:
public class TodoReducer {
public TodoState reduce(TodoState oldState, Action action) {
if(action instanceof AddItemAction) {
AddItemAction addItemAction = (AddItemAction) action;
List<String> oldItems = oldState.getItems();
List<String> newItems = new ArrayList<>(oldItems);
newItems.add(addItemAction.getValue());
return oldState.withItems(newItems);
}
return oldState;
}
}
The implementation is pretty straight forward. If the incoming action is of type AddItemAction
we handle it by
creating a new item list containing the old items together with the new one. Then we create an immutable copy with the
new items by invoking withItems
on the old state instance. If the action doesn't match the desired type we ignore it
and simply return the existing state.
This is the basic redux code for our application. What is missing is the UI component to visualize the app but we will come to this later. Now we can start implementing our Redux library. The first step is to introduce an interface for Reducers. This should look like this:
public interface Reducer<S> {
S reduce(S oldState, Action action);
}
The generic type variable S
is used for the actual type of the State. Now our example reducer can implement the new
interface:
public class TodoReducer implements Reducer<TodoState> {
@Override
public TodoState reduce(TodoState oldState, Action action) {
// ...
}
}
The Redux store
Now we can implement the actual Redux store. The task of the Store is to hold an instance of the current State and to exchange the state every time an action is dispatched. So the basic interface for the Store could look like this:
public class Store<S> {
private S currentState;
private Reducer<S> reducer;
public Store(S initialState, Reducer<S> rootReducer) {
this.currentState = initialState;
this.reducer = rootReducer;
}
public S getState() {
return this.currentState;
}
public void dispatch(Object action) {
// todo
}
}
To create a Store instance we need to provide an initial State and the Reducer. Then you can request the current State
with getState
and dispatch an Action with dispatch
.
Now let's implement the dispatch
method. Notice that I've used the type Object
for the action param instead of our
Action
interface. At first this might look strange but it enables us to create powerful extension mechanisms later on.
These extensions will make it possible to not only dispatch simple action objects but also other things like function
definitions or CompleteableFuture
s. This way it is possible to introduce different strategies to handle asynchronous
behavior that otherwise wouldn't be possible. Of course at this stage dispatching something other than a simple action
object would crash the app with a ClassCastException
. In one of the next blog posts I will introduce the concept of
"middlewares" and then we will fix this issue.
public void dispatch(Object action) {
this.currentState = reducer.reduce(this.currentState, (Action) action);
}
Subscribe to changes
We are now able to dispatch actions and have the reducer creating new states for us. However, this implementation isn't really useful yet. What is missing is a way to get notified about state-changes so that you can re-render the UI. For this I'm introducing two interfaces:
public interface Subscription {
void unsubscribe();
}
public interface Subscriber<S> {
void onChange(S state);
}
Subscriber
is just a regular observer. It's onChange
method will be invoked when the State in the store has been
changed. When subscribing to the Store you pass a Subscriber
as argument and you will get a Subscription
as a
result. You can ignore this Subscription
or use it later to unsubscribe from the Store. This is a nice convenience
feature but if you like you can also just implement a classic observer pattern. Subscribing to the Store looks like
this:
Subscription sub = store.subscribe(state -> {
// do some updates based on the new state
})
// later
sub.unsubscribe();
Now the implementation of the Store get's a little more interesting. First we add a
private List<Subscriber<S>> subscribers = new ArrayList<>();
list to the store. Then we add the subscribe
method:
public Subscription subscribe(Subscriber<S> subscriber) {
subscribers.add(subscriber);
subscriber.onChange(this.currentState);
return () -> {
subscribers.remove(subscriber);
};
}
The subscriber is added to the subscriber list and it's onChange
method is invoked with the current state. This way
the subscribing code can properly initialize itself. Then we create a lambda for the Subscription
which, when invoked,
will remove the subscriber from the list.
The next step is to update the dispatch
method. We need to notify all subscribers when an action is dispatched.
Additionally I'm adding a check to see if the new state was really changed. A friendly reducer returns the existing
state untouched if it doesn't know the incoming Action or it don't want to react to the action. Therefore it's likely
that the resulting state instance is the same as the existing one. In this situation we don't want to notify the
subscribers to minimize unnecessary re-renderings of the UI. The implementation now looks like this:
private void notifySubscribers() {
subscribers.forEach(subscriber -> subscriber.onChange(this.currentState));
}
public void dispatch(Object action) {
S oldState = this.currentState;
S newState = reducer.reduce(this.currentState, (Action) action);
if (oldState != newState && !oldState.equals(newState)) {
this.currentState = newState;
notifySubscribers();
}
}
And that's it. This is a basic but fully functional Redux implementation. In comparison with the original redux.js library it misses some extension APIs like Middlewares and Store-Enhancers but we will work on this in a separate blog post. What is missing now to be useful is the UI component and the connection between the UI and the Redux Store. This will be the topic for the next part of this series.
You can find the code for redux example in this github repository. This commit is pointing directly to the state of this blog post.