Skip to content

Instantly share code, notes, and snippets.

@suneelv
Last active December 3, 2018 07:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save suneelv/1a8066c870c5a45ae2f9a488d00eae27 to your computer and use it in GitHub Desktop.
Save suneelv/1a8066c870c5a45ae2f9a488d00eae27 to your computer and use it in GitHub Desktop.
Using redux in a GWT JsInterop application
import org.jboss.errai.common.client.api.annotations.Portable;
import jsinterop.annotations.JsProperty;
@Portable
public class BaseDTO {
private String id;
private Date created;
private Date modified;
@JsProperty
public void setId() {
this.id = id;
}
@JsProperty
public String getId() {
return this.id;
}
// Similar getters and setters for other properties too.
}
@JsType(name = "CommandHelper", namespace = "GWT")
public class CommandHelper {
public static void newContactGetCommand(String id) {
return new GetCommand(ContactDTO.class, id);
}
}
import jsinterop.annotations.JsMethod;
public class CommunicationManager {
@JsMethod
public <R extends Response> Operation<R> executeCommand(Command<R> command) {
// Contains code that actually sends the command to server
// We use https://github.com/errai/errai library which takes care
// of marshalling and unmarshalling java objects and communication with the server
}
}
import jsinterop.annotations.JsFunction;
// This lets us pass javascript callbacks into java code
@JsFunction
public interface Consumer<T> {
void accept(T data);
}
import org.jboss.errai.common.client.api.annotations.Portable;
import jsinterop.annotations.JsProperty;
@Portable
public class ContactDTO extends BaseDTO {
private String firstName;
private String lastName;
@JsProperty
public void setFirstName() {
this.firstName = firstName;
}
@JsProperty
public String getFirstName() {
return this.firstName;
}
@JsProperty
public void setLastName() {
this.lastName = lastName;
}
@JsProperty
public String getLastName() {
return this.firstName;
}
}
const actionTypes = {
READ_ENTITY : 'READ_ENTITY',
DELETE_ENTITY : 'DELETE_ENTITY',
UPDATE_ENTITY : 'UPDATE_ENTITY'
}
const initialState = {
entities : {},
views : {}
}
// Each entity reducer can have multiple views
// Filters and sort orders for each view are defined in GWT
// The same filters and sort orders are used to filter entities on the client and server
// const exampleState = {
// entities : {
// 'randomId1' : 'GWT Js Interop Object',
// 'randomId2' : 'GWT Js Interop Object',
// 'randomId3' : 'GWT Js Interop Object',
// 'randomId4' : 'GWT Js Interop Object',
// 'randomId5' : 'GWT Js Interop Object',
// 'randomId6' : 'GWT Js Interop Object',
// },
// views : {
// libraryView : {
// ids : ['randomId3, randomId4, randomId5'],
// fetching : false,
// pageSize : 3,
// totalCount : 40
// },
// filteredView : {
// ids : ['randomId1, randomId2'],
// fetching : false,
// pageSize : 3,
// totalCount : 2
// }
// }
// }
// We are using this generic reducer which actually does not care about what the real entity type is
function entityReducer(state = initialState, action) {
switch(action.type) {
case actionTypes.READ_ENTITY: {
const { entity } = action.payload;
const newEntities = { ...state.entities, [`${entity.id}`]: entity };
return {
entities : newEntities,
views : state.views // Actually here we have complex logic to update the entity in views which I have omitted
}
}
case actionTypes.DELETE_ENTITY : {
// Logic to delete entity from entities state and update respective views
}
case actionTypes.UPDATE_ENTITY : {
// Logic to update entity from entities state and update respective views
}
}
}
// This higher order reducer is responsible for sending actions to only reducers that care about the current entity type in action payload
function createEntityReducer(reducerFunction, entityType) {
return (state, action) => {
const isInitializationCall = state === undefined;
const actionIsForReducer = action.payload && action.payload.entityType === entityType;
if (!actionIsForReducer && !isInitializationCall) return state;
return reducerFunction(state, action);
};
}
const entitiesReducer = combineReducers({
contact : createEntityReducer(entityReducer, 'contact'),
user : createEntityReducer(entityReducer, 'user')
});
const store = createStore(entitiesReducer, []);
// In our real application we are using a middleware for this
// which first checks whether contact is already fetched and resolves
// immedietly if found or send a command to the server
// Views use this action creater and use the returned promise
// to wait until contact is loaded or act on errors with loading conntact
export function fetchContact(contactId) {
return (dispatch) => {
const command = GWT.CommandHelper.newContactGetCommand(contactId); // This function is exposed by GWT as we are using jsinterop
// We are using dagger dependecy injection and have a GWT class which provides the core dependency objects
// Assume that `CommunicationManager` below is imported from a different module which is reponsible for getting
// the actual initialized instance.
const operation = CommunicationManager.executeCommand(command);
const contactPromise = new Promise((resolve,reject) => {
operation.execute((entityResponse) => {
resolve(entityResponse);
}, (error) => {
reject(error);
});
})
return contactPromise.then(entityResponse => entityResponse.getEntity()).then(contact => {
dispatch({
type : 'READ_ENNTITY',
payload : {
entity : contact,
entityType : 'contact'
}
});
return contact;
});
}
}
import org.jboss.errai.common.client.api.annotations.Portable;
@Portable
public class EntityResponse<T extends BaseDTO> extends Response {
private T entity;
@JsMethod
public T getEntity() {
return this.entity;
}
public void setEntity(T entity) {
this.entity = entity;
}
}
@Portable
public class GetCommand<T extends BaseDTO> extends Command<EntityResponse<T>> {
private BasEntityClass requestedType;
private String id;
public GetCommand(Class entityClass, String id) {
// ClassHelper is a utility class used by both client and server
// to have a mapping between requested entity type and the actual class
this.requestedType = ClassHelper.getTypeFromClass(entityClass);
this.id = id;
}
}
public abstract class Operation<T> {
@JsMethod
public abstract void execute(Consumer<T> onData, Consumer<Throwable> onError);
@JsMethod
public abstract void cancel();
}
import org.jboss.errai.common.client.api.annotations.Portable;
@Portable
public class Response {
}
import React, { useState, useEffect} from 'react';
import { connect } from 'react-redux';
import { fetchContact } from './entity-reducer';
function _SingleContact({ contactId, dispatch }) {
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchContact(contactId).then(() => {
setLoading(false);
}, (error) => {
// handle error
})
}, [contactId])
if(loading) return null;
return <SingleContactView contactId={contactId} />;
}
const SingleContact = connect()(_SingleContact);
// Contact is the GWT object stored in redux state.
// Since we have proper jsInterop annotations on the properties of the object
// we can directly use `firstName` and `lastName` in javascript
function _SingleContactView = ({ contactId, contact }) {
return (
<div>
<div>{contact.firstName}</div>
<div>{contact.lastName}</div>
</div>
)
}
function mapStateToProps(state, ownProps) {
return {
contact : selectEntity(state, ownProps.contactId, 'contact'); // Selector responsible for plucking contact out of state
}
}
const SingleContactView = connect(mapStateToProps)(_SingleContactView)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment