www.ostrowski.ninja
Project with triple inheritance of React class components
Refactoring (noun): a change made to the internal structure of software to make it easier to understand and cheaper to modify without changing its observable behavior.
3️⃣ The third time you do something similar, you refactor.
2️⃣ The second time you do something similar, you wince at the duplication, but you do the duplicate thing anyway.
1️⃣ The first time you do something, you just do it.
(Dead code, long classes, global variables etc)
👶 Inexperienced developers
🚽 Tech debt (strategic or not)
📋 Catching up with business requirements
🤔 Not enough information about domain
🌱 It just happens when code grows. And it's normal.
const LoginForm = () => {
const handleSubmit = event => {
const values = getValuesFromEvent(event); // Call some util
localStorage.setItem('form', values);
fetch('/api', { body: JSON.stringify(values) }).then(
() => {
// Handle success
},
).catch(err => {
// Handle error
});
// And some other domain/state/business logic
};
return (
<form onSubmit={handleSubmit}>
<input type="email"/>
<input type="password"/>
<button type="submit">Log in</button>
</form>
);
};
Don't mix business logic with UI
type Props = {
onSubmit(values: { email: string; password: string }): unknown;
};
const LoginForm = ({onSubmit}: Props) => {
const handleSubmit = event => {
const values = getValuesFromEvent(event); // Call some util
onSubmit(values)
};
return (
<form onSubmit={handleSubmit}>
<input type="email"/>
<input type="password"/>
<button type="submit">Log in</button>
</form>
);
};
Provide data and emit only callbacks / events.
const ComposedLoginForm = compose(
withLocalStorage,
withSavingToApi
)(LoginForm)
With HOC each piece of logic can be extracted to separate function
const CollapseSection = ({ headline, content }) => {
const [open, setOpen] = useState(false);
return (
<div>
<button onClick={
() => setOpen(open => !open)}>
{headline}
</button>
{open && <div>{content}</div>}
</div>
);
};
This will not work if you need to control state from parent or Redux.
type CollapseProps = {
headline: ReactNode;
content: ReactNode;
};
type ToggleProps = {
open: boolean;
onToggle(): unknown;
};
const CollapseSection = ({
headline,
content,
open,
onToggle,
}: ToggleProps & CollapseProps) => (
<div>
<button onClick={onToggle}>{headline}</button>
{open && <div>{content}</div>}
</div>
);
const withToggleState = <Props extends ToggleProps>(
Component: ComponentType<Props>,
) => (props: Props) => {
const [open, setOpen] = useState(false);
return <Component
open={open}
onToggle={() => setOpen(open => !open)}
{...props}
/>;
};
const StatefulCollapseSection =
withToggleState(CollapseSection);
const StatefulModal =
withToggleState(SomeModalComponent);
This will allow both controlled and stateful components to work together
import { compose, withState, withHandlers } from 'recompose';
type Props = {
count: number;
increment(): unknown;
decrement(): unknown;
};
const PureCounter = ({ count, increment, decrement }: Props) => (
<div>
{count}
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
const withCounter = compose<Props, {}>(
withState('counter', 'setCounter', 0),
withHandlers({
increment: ({ setCounter }) => () => setCounter(n => n + 1),
decrement: ({ setCounter }) => () => setCounter(n => n - 1),
}),
);
const StatefulCounter = withCounter(PureCounter);
With recompose helpers
you can create HOCs easily
const FileUploader = ({ onUpload }) => {
const inputRef = useRef<HTMLInputElement | null>(null);
const [selectedFile, setSelectedFile] = useState();
const processFile = (file: File) => {
// Some file logic
};
const onFileUploadRequested = () => {
inputRef.current.click();
};
const handleFileUpload = (event: ChangeEvent<HTMLInputElement>) => {
setSelectedFile(processFile(event.target.files[0]));
};
const onSubmit = () => {
onUpload(selectedFile);
};
return (
<div>
<button onClick={onFileUploadRequested}>Upload</button>
<input type="file" hidden ref={inputRef} onChange={handleFileUpload}/>
<button onClick={onSubmit}>Submit</button>
</div>
);
};
Sometimes view require a lot of boilerplate to make it work
const useFileUpload = () => {
const inputRef =
useRef<HTMLInputElement | null>(null);
const [selectedFile, setSelectedFile] =
useState();
const processFile = (file: File) => {
// Some file logic
};
const onFileUploadRequested = () => {
inputRef.current.click();
};
const handleFileUpload =
(event: ChangeEvent<HTMLInputElement>) => {
setSelectedFile(processFile(event.target.files[0]));
};
return {
inputRef,
selectedFile,
setSelectedFile,
onFileUploadRequested,
handleFileUpload,
};
};
const FileUploader = ({ onUpload }) => {
const { onFileUploadRequested,
selectedFile, inputRef,
handleFileUpload } = useFileUpload();
const onSubmit = () => {
onUpload(selectedFile);
};
return (
<div>
<button
onClick={onFileUploadRequested}>
Upload
</button>
<input
type="file" hidden ref={inputRef}
onChange={handleFileUpload}
/>
<button
onClick={onSubmit}>
Submit
</button>
</div>
);
};
You can hide some code in Hook
and expose only public fields
export const SomeBiggerComponent = ({someProp}) => {
return (
<div>
<div>
<h1>Some headline</h1>
</div>
<div>
<p>Some text</p>
</div>
{someProp ? (
<div>Some optional content rendered conditionally</div>
) : (
<div>Or another content rendered otherwise</div>
)}
<div>
{/* And more, and more, and more */}
</div>
</div>
)
}
Code nesting and ternaries in big JSX templates can be hard to read
export const SomeBiggerComponent = ({ someProp }) => {
const renderContentA = () => (
<div>Some optional content rendered conditionally</div>
);
const renderContentB = () => (
<div>Or another content rendered otherwise</div>
);
const renderContent = () => {
return someProp ? renderContentA() : renderContentB();
}
return (
<div>
<div>
<h1>Some headline</h1>
</div>
<div>
<p>Some text</p>
</div>
{renderContent()}
<div>
{/* And more, and more, and more */}
</div>
</div>
);
};
Extracting jsx to partials
give them semantic value (names) and are easier to move to components later
import { TextField } from '@material-ui/core';
export const Form = () => (
<form>
<TextField
className="input"
variant="outlined"
size="large"
label="Email"
/>
<TextField
className="input"
variant="outlined"
size="large"
label="Password"
/>
<TextField
className="input"
variant="outlined"
size="large"
label="First Name"
/>
<TextField
className="input"
variant="outlined"
size="large"
label="Last Name"
/>
</form>
);
We waste at least 9 lines of code by duplication same props
import { TextField } from '@material-ui/core';
import { mapProps } from 'recompose';
const MyTextField = mapProps<TextFieldProps, TextFieldProps>(props => ({
className: 'input',
variant: 'outlined',
size: 'large',
...props
}))(TextField);
export const Form = () => (
<form>
<MyTextField
label="Email"
/>
<MyTextField
label="Password"
/>
<MyTextField
label="First Name"
/>
<MyTextField
label="Last Name"
/>
</form>
);
We can create component with
mapped props to avoid duplication
class Component extends React.Component {
componentDidMount() {
// Handle DOM etc
}
componentDidUpdate(prevProps) {
if(prevProps.someProp !== this.props.someProp) {
// Handle prop changes and do something
}
}
componentWillUnmount() {
// Clear after component
}
render() {
return <></>
}
}
Lifecycle methods require us to understand React "internals"
const Component = (props) => {
useEffect(() => {
// Handle DOM etc
return () => {
// Cleanup after component
}
}, []);
useEffect(() => {
// Handle prop changes and do something
}, [props.someProp]);
return <></>
}
With hooks we sync to state changes and focus on our data
⏱ Sagas are great not only to handle async operations
🙌 Perfect spot to abstract away business logic from view
👮♂️ Logic in Sagas enhances CQRS and event driven architecture
🥰 Sagas improves code reusability (eg. shared threads in React and React Native)
import { loginFormSubmitted } from './login-form-actions';
const LoginForm = () => {
const dispatch = useDispatch();
const handleSubmit = (email, password) => {
dispatch(
loginFormSubmitted({ email, password }),
);
};
return (
<form onSubmit={handleSubmit}>
{/* Some form HTML */}
</form>
);
}
Component just emit what happened in its context.
import { put, take, call } from '@redux-saga/effects';
import { push } from 'connected-react-router';
import { Routes } from '../../routes';
import { loginFormSubmitted } from './login-form-actions';
import { apiLoginRequested, apiLoginSucceed } from '../api/login/actions';
import {storageService} from '../../storage/storage-service'
export function* loginUserSaga(action: ReturnType<typeof loginFormSubmitted>) {
const { email, password } = action.payload;
yield put(apiLoginRequested(email, password));
const loginResultAction = yield take(apiLoginSucceed);
// Storage can be in saga too! Loose coupling FTW
yield call(storageService, 'session', loginResultAction.payload);
yield put(
push(Routes.APP),
);
}
Saga react's on events and control
business/app logic
const Nav = ({ isCheckout }) => {
if (!isCheckout) {
return (
<nav>
<div>
<Logo/>
</div>
<div>
<Menu/>
</div>
</nav>
);
// We don't want menu in checkout
} else {
return (
<nav>
<div>
<Logo/>
</div>
</nav>
);
}
};
We don't want component to know about where they are placed.
const Nav = ({ leftSlot = null, centerSlot = null, rightSlot = null }) => {
return (
<nav>
{leftSlot && <div>{leftSlot}</div>}
{centerSlot && <div>{centerSlot}</div>}
{rightSlot && <div>{rightSlot}</div>}
</nav>
);
};
const StandardNav = () => (
<Nav leftSlot={<Logo/>} rightSlot={<Menu/>}/>
);
const CheckoutNav = () => (
<Nav centerSlot={<Logo/>} />
);
We can compose different variants using slots
👮♂️ Can cause performance issues
🍾 Component can pass data to parent's function
🤝 Component accept function as a prop
🤼♂️ Useful to decouple UI from state or other data
⚒️ Very useful for utilities and UI libraries
import ReactFlagsSelect from 'react-flags-select';
export const Form = () => {
return (
<ReactFlagsSelect
searchable={true}
searchPlaceholder="Search for a country"/>
);
};
This component force me to use
its ugly UI
interface Props {
countries?: string[];
blackList?: boolean;
customLabels?: {[propName: string]: string};
selectedSize?: number;
optionsSize?: number;
defaultCountry?: string;
placeholder?: string;
className?: string;
showSelectedLabel?: boolean;
showOptionLabel?: boolean;
alignOptions?: string;
onSelect?: (countryCode: string) => void;
disabled?: boolean;
searchable?: boolean;
}
Its props don't let me style it
how I need
import ReactFlagsSelect from 'react-flags-select';
import { MyListItem, MyInput } from '@app/components';
export const Form = () => {
return (
<ReactFlagsSelect
renderInput={inputProps => <MyInput {...inputProps} />}
renderFlag={({ flagImage, countryName }) => (
<MyListItem image={flagImage} label={countryName}/>
)}
/>
);
};
With render props I can provide my own UI with data exposed from component
const Component = ({ componentData }) => {
// We have to check every component...
if (!componentData) {
return <Loader/>;
}
return <div>{componentData.something}</div>;
};
We are not DRY - we need to check every component if data exist
import { branch, renderComponent } from 'recompose';
const Component = ({ componentData }) => {
// Now SRP - just dumb render
return <div>{componentData.something}</div>;
};
const hasRequiredData = props => Boolean(props.componentData);
export const ComponentWithLoading = branch(
// If test pass
props => !hasRequiredData(props),
// run this:
renderComponent(Loader)
// Otherwise just pass Component
)(Component);
We recompose branch HOC we can
move this check to other level
🧐 "Prefer duplication over the wrong abstraction"
💵 "Duplication is far cheaper than the wrong abstraction"
⛔️ "AHA Programming = Avoid Hasty Abstractions"
👍 "Optimize for change first"
Remember that you should refactor
when you see it's needed. Don't do it prematurely!
❗ Remember ❗
💪 Use Typescript
⚒️ Use helpful utils like Recompose
🏑 Use new features like Hooks API
🥇 Try to make your components single-responsibility
🎠 Embrace good event-driven architecture with Saga
📈 Decouple business from UI
🧮 Embrace functional programming patterns