The non-React FP example
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.
Functional isValid
There are a few ways to order the logic here, but we'll go with this:
- Given a list of ids
- All ids are valid if there does not exist an invalid id
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
Functional render
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:
- it takes two inputs (the left operand and the right operand)
- it returns a result (for strings, the concatenation of the operands)
- it has no side effects
- it doesn't mutate its inputs (the result is a new string--the operands are unchanged)
So, with that, render is now functional!
What about ajax?
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.
Applying it in React
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
has-arelation). Both components can be and usually are of different type.