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