3.6 Encapsulation & External Interfaces
Now, the required UI & functionalities of our RandomGif
component have all been completed. However, we still need to do the followings to make sure the component is easy to be reused:
3.6.1 Define External Action Interfaces
3.6.1.1 Incoming Actions / allowedIncomingMulticastActionTypes
By default, the Component Container
won't accept any multicast actions (non-multicast & direct addressed actions will always be accepted though). To make sure the component users can send REQUEST_NEW_GIF
actions to this component in order to programmingly trigger the image loading, we can set the incoming actions rulse using allowedIncomingMulticastActionTypes
option when register our component:
this.componentManager = new ComponentManager(this, {
namespace: "io.github.t83714/RandomGif",
actionTypes,
reducer,
saga,
// --- Only allow `actionTypes.REQUEST_NEW_GIF` to be received by component
allowedIncomingMulticastActionTypes: [actionTypes.REQUEST_NEW_GIF],
namespaceInitCallback: componentManager => {
const styleSheet = jss
.setup(jssDefaultPreset())
.createStyleSheet(styles, {
generateClassName: componentManager.createClassNameGenerator()
})
.attach();
return { styleSheet };
},
namespaceDestroyCallback: ({ styleSheet }) => {
styleSheet.detach();
}
});
If you want your component to accept any actions, you can set allowedIncomingMulticastActionTypes
to "*"
.
You should avoid setting
allowedIncomingMulticastActionTypes
to"*"
unless you tried to create aforwarder
type component. One example is the ToggleButton
3.6.1.2 Outgoing External Actions
It would be useful if our component sends out actions at different stages of requesting data from remote API so that other part of the program can monitor the actions and can act accordingly. We are going to define the following Outgoing External Actions
(in src/RandomGif/actions/types.js
):
// --- dispatch when a NEW Random Gif URl is received from API
export const NEW_GIF = Symbol("NEW_GIF");
// --- dispatch when a request is just sent
export const LOADING_START = Symbol("LOADING_START");
// --- dispatch when a request is complete
export const LOADING_COMPLETE = Symbol("LOADING_COMPLETE");
And, we need to define the following action creator functions accordingly (in src/RandomGif/actions/index.js
):
export function newGif() {
return {
type: actionTypes.NEW_GIF
};
}
export function loadingStart() {
return {
type: actionTypes.LOADING_START
};
}
export function loadingComplete(error = null) {
return {
type: actionTypes.LOADING_COMPLETE,
payload: {
isSuccess: error ? false : true,
error
}
};
}
Next, we can modify saga
to dispatch those actions at appropriate places:
const mainSaga = function*(effects) {
try{
while(true){
/**
* Dedicated LOADING_START action to notify interested components outside
* This component will not use it in any way
*/
yield effects.put(actions.loadingStart(), "../../../*");
// --- create an effect of taking an `REQUEST_NEW_GIF` action from this Component's action channel
// --- Until an action is available for being taken, this saga will not resume its execution.
const action = yield effects.take(actionTypes.REQUEST_NEW_GIF);
const response = yield effects.call(fetchGif, "Y4P38sTJAgEBOHP1B3sVs0Jtk01tb6fA");
const imgUrl = response.data.fixed_width_small_url;
yield effects.put(actions.receiveNewGif(imgUrl));
/**
* The optional second `relativeDispatchPath` parameter defines
* the relative (from current component full namespace path) namespace dispatch path.
* i.e. suppose the current component full namespace path is:
* namespacePrefix namespace random component ID
* `xxxxxx` / `io.github.t83714/RandomGif` / `cx`
* e.g. `exampleApp/Gifs/io.github.t83714/RandomGif/c0`
* If `relativeDispatchPath` is `../../../*`, the effective dispatch path is `exampleApp/Gifs/*`.
* Although, theoretically, you could use `relativeDispatchPath` & ".." to dispatch the action
* into any namespace, you should throw the action just out of the your component
* as you are supposed to know nothing about outside world as a component author.
*/
//--- optional second `relativeDispatchPath` parameter
//--- specify the action dispatch path
yield effects.put(actions.newGif(), "../../../*");
/**
* Dedicated LOADING_COMPLETE action to notify interested components outside
*/
yield effects.put(actions.loadingComplete(), "../../../*");
}
}catch(e){
yield effects.put(actions.requestNewGifError(e));
/**
* Dedicated LOADING_COMPLETE action to notify interested components outside
* This component will not use it in any way
*/
yield effects.put(actions.loadingComplete(e), "../../../*");
}
};
3.6.1.3 Export External Actions Types
We will also want to export those external actions types (in src/RandomGif/index.js
) for potential component users:
//--- actions component may send out
const exposedActionTypes = {
NEW_GIF: actionTypes.NEW_GIF,
LOADING_START: actionTypes.LOADING_START,
LOADING_COMPLETE: actionTypes.LOADING_COMPLETE,
REQUEST_NEW_GIF: actionTypes.REQUEST_NEW_GIF
};
//--- action component will accept
const exposedActions = {
requestNewGif: actions.requestNewGif
};
/**
* expose actions for component users
*/
export { exposedActionTypes as actionTypes, exposedActions as actions };
3.6.1.4 Class.contextType
This section is only required for React Class Component. React Function Component use Hook APIs to retrieve context and doesn't require setting
Class.contextType
.
The contextType
property of the React Class Component needs to be assigned a Context object AppContainerContext
in order to make the React Class Component to receive AppContainer
instance from context.
Here is an example:
import React from "react";
import { ComponentManager, AppContainerContext } from "fractal-component";
class RandomGif extends React.Component {
constructor(props) {
super(props);
this.componentManager = new ComponentManager(this, {
namespace: "io.github.t83714/RandomGif"
});
}
render() {
return <div>Hello from RandomGif!</div>;
}
}
// --- Set contentType allow `AppContainer` pass through React Context
RandomGif.contextType = AppContainerContext;
export default RandomGif;
3.6.2 Component Config Properties
We will want to define the following React Component Properties to allow users to config our component's functionalities:
RandomGif.propTypes = {
// --- User can set `showButton` to false to hide the button
showButton: PropTypes.bool,
// --- Allow component user to set their own Giphy.com API key
apiKey: PropTypes.string,
// --- Allow user replace component built stylesheet
styles: PropTypes.object,
/*
* Allow user specify the `AppContainer` for this component through component property
*/
appContainer: PropTypes.instanceOf(AppContainer)
};
// --- set component properties default value
RandomGif.defaultProps = {
showButton: true,
apiKey: "Y4P38sTJAgEBOHP1B3sVs0Jtk01tb6fA"
};
Component users generally will not want to provide
AppContainer
instance explicitly through Component PropsappContainer
. Passing it through React Context will always be easier. Moreover, if you don't create anAppContainer
instance, Component Manager will auto-create one during the initialisation and share among all components.Besides the component properties defined above, users can always set
namespacePrefix
property to set thenamespace prefix
of the component.
3.6.2.1 showButton
To hide Get Gif
button depends on showButton
property, we can update Get Gif
button JSX in render()
to the followings:
{this.props.showButton && (
<div className={`${classes.cell} `}>
<button
onClick={() => {
this.componentManager.dispatch(
actions.requestNewGif()
);
}}
disabled={this.state.isLoading}
>
{this.state.isLoading
? "Requesting API..."
: "Get Gif"}
</button>
</div>
)}
3.6.2.2 apiKey
To allow users to update apiKey
, we need to update saga
to the followings:
const mainSaga = function*(effects, apiKey) {
...
const response = yield effects.call(fetchGif, apiKey);
...
};
And then, we need to prefill
the last parameter of the saga
when register the component:
import partialRight from "lodash/partialRight";
...
this.componentManager = new ComponentManager(this, {
...
saga: partialRight(saga, props.apiKey),
...
});
3.6.2.3 styles
In order to allow users to override the component built-in stylesheet, we need to update the namespaceInitCallback
& namespaceDestroyCallback
to the followings:
this.componentManager = new ComponentManager(this, {
...
namespaceInitCallback: componentManager => {
let jssRef;
if(!props.styles){
// --- if use built-in style, we want to make sure
// --- that this component use its own jss setting
jssRef = jss.setup(jssDefaultPreset());
}else{
jssRef = jss
}
const styleSheet = jssRef
.createStyleSheet(props.styles ? props.styles : styles, {
// --- use componentManager's createClassNameGenerator
generateClassName: componentManager.createClassNameGenerator()
})
.attach();
return { styleSheet };
},
namespaceDestroyCallback: ({ styleSheet }) => {
styleSheet.detach();
}
});