I'm currently working on a ReactJS project where I need to create "re-usable" components in which some of the methods would need to be "overridden". In OOP I would use polymorphism. I've done some reading and it seems the consensus is to use HoC/composition but I can't quite figure out how to achieve this. I figure if I could just get a ES6 sample using composition it would maybe be easier to adapt the idea to ReactJS afterwards.
Below is a ES6 OOP example (ignore the event handling it's just for testing) of pretty much what I would like to achieve in ReactJS. Does anyone have some guidance on how to break up a ReactJS component into a HoC or even just demonstrate how I would go about using composition in ES6 based on the example?
class TransferComponent {
constructor(){
let timeout = null;
this.render();
this.events();
}
events(){
let scope = this;
document.getElementById('button').addEventListener('click', function(){
scope.validate.apply(scope);
});
}
validate(){
if(this.isValid()){
this.ajax();
}
}
isValid(){
if(document.getElementById('username').value !== ''){
return true;
}
return false;
}
ajax(){
clearTimeout(this.timeout);
document.getElementById('message').textContent = 'Loading...';
this.timeout = setTimeout(function(){
document.getElementById('message').textContent = 'Success';
}, 500);
}
render(){
document.getElementById('content').innerHTML = '<input type="text" id="username" value="username"/>\n\
<button id="button" type="button">Validate</button>';
}
}
class OverrideTransferComponent extends TransferComponent{
isValid(){
if(document.getElementById('username').value !== '' && document.getElementById('password').value !== ''){
return true;
}
return false;
}
render(){
document.getElementById('content').innerHTML = '<input type="text" id="username" value="username"/>\n\
<input type="text" id="password" value="password"/>\n\
<button id="button" type="button">Validate</button>';
}
}
const overrideTransferComponent = new OverrideTransferComponent();
<div id="content"></div>
<div id="message"></div>
UPDATE: Even though my original question was around FP I think render props is a really good solution to my issue and avoids the HoC issues.
This is post is out of date. Hooks are better for most use cases.
The answer regarding your example code is in the middle/bottom of this post.
A good way to go about React composition is the render-callback pattern, aka function-as-child. The main advantage it has over HOC's is that it allows you to compose components dynamically at runtime (e.g. in render) instead of statically at author time.
Regardless of whether you use render-callback or HOC's, the goal in component composition is to delegate reusable behavior to other components, and then pass those components in as props to the component that needs them.
The following Delegator
component uses the render-callback pattern to delegate implementation logic to an ImplementationComponent
that is passed in as a prop:
const App = () => <Delegator ImplementationComponent={ImplementationB} />;
class Delegator extends React.Component {
render() {
const { ImplementationComponent } = this.props;
return (
<div>
<ImplementationComponent>
{ ({ doLogic }) => {
/* ... do/render things based on doLogic ... */
} }
</ImplementationComponent>
</div>
);
}
}
The various implementation components would look like:
class ImplementationA extends React.Component {
doSomeLogic() { /* ... variation A ... */ }
render() {
this.props.children({ doLogic: this.doSomeLogic })
}
}
class ImplementationB extends React.Component {
doSomeLogic() { /* ... variation B ... */ }
render() {
this.props.children({ doLogic: this.doSomeLogic })
}
}
Later down the line, you could nest more child components in the Delegator
component following the same compositional pattern:
class Delegator extends React.Component {
render() {
const { ImplementationComponent, AnotherImplementation, SomethingElse } = this.props;
return (
<div>
<ImplementationComponent>
{ ({ doLogic }) => { /* ... */} }
</ImplementationComponent>
<AnotherImplementation>
{ ({ doThings, moreThings }) => { /* ... */} }
</AnotherImplementation>
<SomethingElse>
{ ({ foo, bar }) => { /* ... */} }
</SomethingElse>
</div>
);
}
}
Now nested child component allows for multiple concrete implementations:
const App = () => (
<div>
<Delegator
ImplementationComponent={ImplementationB}
AnotherImplementation={AnotherImplementation1}
SomethingElse={SomethingVariationY}
/>
<Delegator
ImplementationComponent={ImplementationC}
AnotherImplementation={AnotherImplementation2}
SomethingElse={SomethingVariationZ}
/>
</div>
);
Applying the above composition pattern to your example, the solution restructures your code but assumes that it needs to do the following:
Firstly, to make things easier I changed the DOM to:
<div id="content-inputs"></div>
<div id="content-button"></div>
Now, the TransferComponent
knows only how to display a button and do something when the button is pressed and the data is valid. It doesn't know what inputs to display or how to validate the data. It delegates that logic to the nested VaryingComponent
.
export default class TransferComponent extends React.Component {
constructor() {
super();
this.displayDOMButton = this.displayDOMButton.bind(this);
this.onButtonPress = this.onButtonPress.bind(this);
}
ajax(){
console.log('doing some ajax')
}
onButtonPress({ isValid }) {
if (isValid()) {
this.ajax();
}
}
displayDOMButton({ isValid }) {
document.getElementById('content-button').innerHTML = (
'<button id="button" type="button">Validate</button>'
);
document.getElementById('button')
.addEventListener('click', () => this.onButtonPress({ isValid }));
}
render() {
const { VaryingComponent } = this.props;
const { displayDOMButton } = this;
return (
<div>
<VaryingComponent>
{({ isValid, displayDOMInputs }) => {
displayDOMInputs();
displayDOMButton({ isValid });
return null;
}}
</VaryingComponent>
</div>
)
}
};
Now we create concrete implementations of the VaryingComponent
to flesh out various input display and validation logics.
The username-only implementation:
export default class UsernameComponent extends React.Component {
isValid(){
return document.getElementById('username').value !== '';
}
displayDOMInputs() {
document.getElementById('content-inputs').innerHTML = (
'<input type="text" id="username" value="username"/>'
);
}
render() {
const { isValid, displayDOMInputs } = this;
return this.props.children({ isValid, displayDOMInputs });
}
}
The username-and-password implementation:
export default class UsernamePasswordComponent extends React.Component {
isValid(){
return (
document.getElementById('username').value !== '' &&
document.getElementById('password').value !== ''
);
}
displayDOMInputs() {
document.getElementById('content-inputs').innerHTML = (
'<input type="text" id="username" value="username"/>\n\
<input type="text" id="password" value="password"/>\n'
);
}
render() {
const { isValid, displayDOMInputs } = this;
return this.props.children({ isValid, displayDOMInputs });
}
}
Finally, composing instances of the TansferComponent
would look like:
<TransferComponent VaryingComponent={UsernameComponent} />
<TransferComponent VaryingComponent={UsernamePasswordComponent} />
For starters, in Functional Programming, the function is a first class citizen. This means you can treat functions as you would data in OOP (i.e. pass as parameters, assign to variables, etc.).
Your example comingles data with behavior in the objects. In order to write a purely functional solution, we'll want to separate these.
Functional Programming is fundamentally about separating data from behavior.
So, let's start with isValid
.
There are a few ways to order the logic here, but we'll go with this:
Which, in JS, translates to:
const areAllElementsValid = (...ids) => !ids.some(isElementInvalid)
We need a couple helper functions to make this work:
const isElementInvalid = (id) => getValueByElementId(id) === ''
const getValueByElementId = (id) => document.getElementById(id).value
We could write all of that on one line, but breaking it up makes it a bit more readable. With that, we now have a generic function that we can use to determine isValid
for our components!
areAllElementsValid('username') // TransferComponent.isValid
areAllElementsValid('username', 'password') // TransferOverrideComponent.isValid
I cheated a little on isValid
by using document
. In true functional programming, functions should be pure. Or, in other words, the result of a function call must only be determined from its inputs (a.k.a. it is idempotent) and it cannot have side effects.
So, how do we render to the DOM without side effects? Well, React uses a virtual DOM (a fancy data structure that lives in memory and is passed into and returned from functions to maintain functional purity) for the core library. React's side effects live in the react-dom
library.
For our case, we'll use a super simple virtual DOM (of type string
).
const USERNAME_INPUT = '<input type="text" id="username" value="username"/>'
const PASSWORD_INPUT = '<input type="text" id="password" value="password"/>'
const VALIDATE_BUTTON = '<button id="button" type="button">Validate</button>'
These are our components--to use the React terminology--which we can compose into UIs:
USERNAME_INPUT + VALIDATE_BUTTON // TransferComponent.render
USERNAME_INPUT + PASSWORD_INPUT + VALIDATE_BUTTON // TransferOverrideComponent.render
This probably seems like an oversimplification and not functional at all. But the +
operator is in fact functional! Think about it:
So, with that, render
is now functional!
Unfortunately, we can't perform an ajax call, mutate the DOM, set up event listeners, or set timeouts without side effects. We could go the complex route of creating monads for these actions, but for our purposes, suffice it to say that we'll just keep using the non-functional methods.
Here's a rewrite of your example using common React patterns. I'm using controlled components for the form inputs. The majority of the functional concepts we've talked about really live under the hood in React, so this is a pretty simple implementation that doesn't use anything fancy.
class Form extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: false,
success: false
};
}
handleSubmit() {
if (this.props.isValid()) {
this.setState({
loading: true
});
setTimeout(
() => this.setState({
loading: false,
success: true
}),
500
);
}
}
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
{this.props.children}
<input type="submit" value="Submit" />
</form>
{ this.state.loading && 'Loading...' }
{ this.state.success && 'Success' }
</div>
);
}
}
The use of state
probably seems like a side effect, doesn't it? In some ways it is, but digging into the React internals may reveal a more functional implementation than can be seen from our single component.
Here's the Form
for your example. Note that we could handle submission in a couple different ways here. One way is to pass the username
and password
as props into Form
(probably as a generic data
prop). Another option is to pass a handleSubmit
callback specific to that form (just like we're doing for validate
).
class LoginForm extends React.Component {
constructor(props) {
super(props);
this.state = {
username: '',
password: ''
};
}
isValid() {
return this.state.username !== '' && this.state.password !== '';
}
handleUsernameChange(event) {
this.setState({ username: event.target.value });
}
handlePasswordChange(event) {
this.setState({ password: event.target.value });
}
render() {
return (
<Form
validate={this.isValid}
>
<input value={this.state.username} onChange={this.handleUsernameChange} />
<input value={this.state.password} onChange={this.handlePasswordChange} />
</Form>
);
}
}
You could also write another Form
but with different inputs
class CommentForm extends React.Component {
constructor(props) {
super(props);
this.state = {
comment: ''
};
}
isValid() {
return this.state.comment !== '';
}
handleCommentChange(event) {
this.setState({ comment: event.target.value });
}
render() {
return (
<Form
validate={this.isValid}
>
<input value={this.state.comment} onChange={this.handleCommentChange} />
</Form>
);
}
}
For sake of example, your app can render both Form
implementations:
class App extends React.Component {
render() {
return (
<div>
<LoginForm />
<CommentForm />
</div>
);
}
}
Finally, we use ReactDOM
instead of innerHTML
ReactDOM.render(
<App />,
document.getElementById('content')
);
The functional nature of React is often hidden by using JSX. I encourage you to read up on how what we're doing is really just a bunch of functions composed together. The official docs cover this pretty well.
For further reading, James K. Nelson has put together some stellar resources on React that should help with your functional understanding: https://reactarmory.com/guides/learn-react-by-itself/react-basics
Reading your question, it's not clear if you are referring to composition or inheritance, but they are different OOP concepts. I recommend you to take a look at this article if you don't know the difference between them.
About your specific problem with React. I'd recommend you to try to user composition since it gives you a lot of flexibility to build your UI and pass props around.
If you are working with React you are probably already using composition when you populate Dialogs dynamically, for example. As the React docs shows:
function FancyBorder(props) {
return (
<div className={'FancyBorder FancyBorder-' + props.color}>
{props.children}
</div>
);
}
function WelcomeDialog() {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
Welcome
</h1>
<p className="Dialog-message">
Thank you for visiting our spacecraft!
</p>
</FancyBorder>
);
}
Folks at Facebook have been developing very challenging UIs and building thousand of components with React and didn't find a good use case for inheritance over composition. As the docs says:
React has a powerful composition model, and we recommend using composition instead of inheritance to reuse code between components.
If you really want to use inheritance, their recommendation is that you extract the functionality that you want to reuse over components into a separate JavaScript module. The components may import it and use that function, object, or a class, without extending it.
In the example you provided, two functions in a utils.js
would do just fine. See:
isUsernameValid = (username) => username !== '';
isPasswordValid = (password) => password !== '';
You can import them and use in your components just fine.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With