Last active
December 3, 2018 07:44
-
-
Save suneelv/1a8066c870c5a45ae2f9a488d00eae27 to your computer and use it in GitHub Desktop.
Using redux in a GWT JsInterop application
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@JsType(name = "CommandHelper", namespace = "GWT") | |
public class CommandHelper { | |
public static void newContactGetCommand(String id) { | |
return new GetCommand(ContactDTO.class, id); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import jsinterop.annotations.JsFunction; | |
// This lets us pass javascript callbacks into java code | |
@JsFunction | |
public interface Consumer<T> { | |
void accept(T data); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
}); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public abstract class Operation<T> { | |
@JsMethod | |
public abstract void execute(Consumer<T> onData, Consumer<Throwable> onError); | |
@JsMethod | |
public abstract void cancel(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import org.jboss.errai.common.client.api.annotations.Portable; | |
@Portable | |
public class Response { | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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