Advanced React Workshop
Richard Lindsey @Velveeta
Lesson 1
Render Callbacks
Render Callbacks
- Allow separation of business logic from presentation
- Useful pattern for both visual and service components
- Define the core of what your component wants governance over, and inject those aspects into functions that return the view
- Render function can be either a standard prop or the 'children' prop if passed between the tags
Render Callbacks
const ResizeObserver = class extends React.Component {
_resizeObserver = new ResizeObserverPolyfill((entries) => {
const [entry] = entries;
this._updateFromDOMRect(entry.contentRect);
});
state = {
height: null,
ref: React.createRef(),
width: null,
};
componentDidMount() {
this._resizeObserver.observe(this.state.ref.current);
this._updateFromDOMRect(this.state.ref.current.getBoundingClientRect());
}
componentWillUnmount() {
this._resizeObserver.disconnect();
}
_updateFromDOMRect(rect) {
this.setState({
height: rect.height,
width: rect.width,
});
}
render() {
return this.props.children(this.state);
}
};
Render Callbacks
const MyResizeableComponent = () => (
<ResizeObserver>
{({ height, ref, width }) => (
<div ref={ref}>
My dimensions are {width}x{height} pixels!
</div>
)}
</ResizeObserver>
);
Lesson 2
Data and Service Providers
Data and Service Providers
- Using components as idioms to express your data and service needs helps your application read more intuitively
- React's context Providers and Consumers can allow you to publish data and request functions to any number of downstream consumers, cache strategies, pooling, etc
- Component-based services allow for centralizing maintenance concerns and simple drop-in consumption of services
Data and Service Providers
const UserDataContext = React.createContext();
const { Provider } = UserDataContext;
const UserDataProvider = class extends React.Component {
_poolingTimeoutId = null;
_queuedRequestIds = [];
state = {
users: {},
};
componentWillUnmount() {
if (this._poolingTimeoutId) {
clearTimeout(this._poolingTimeoutId);
}
}
_issueRequest() {
fetch('/users', {
userIds: this._queuedRequestIds,
})
.then(resp => resp.json())
.then(users => {
this.setState(state => ({
...state,
users: users.reduce((acc, user) => {
acc[user.userId] = user;
return acc;
}, { ...state.users }),
}))
});
this._queuedRequestIds = [];
}
_request = (userId) => {
if (!this.state.users[userId]) {
this._queuedRequestIds.push(userId);
if (!this._poolingTimeoutId) {
this._poolingTimeoutId = setTimeout(() => {
this._poolingTimeoutId = null;
this._issueRequest();
}, this.props.poolingTimeout);
}
}
};
render() {
const value = {
request: this._request,
users: this.state.users,
}
return <Provider value={value}>{this.props.children}</Provider>;
}
};Data and Service Providers
const UserDataConsumer = class extends React.Component {
static contextType = UserDataContext;
componentDidMount() {
this.context.request(this.props.userId);
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.context.request(this.props.userId);
}
}
render() {
const user = this.context.users[this.props.userId];
const props = {
isLoading: !user,
user,
};
return this.props.children(props);
}
};
const UserInfo = ({ userId }) => (
<UserDataConsumer userId={userId}>
{({ isLoading, user }) => (
{isLoading
? <LoadingSpinner />
: <UserProfile user={user} />}
)}
</UserDataConsumer>
);
const UserList = ({ userIds }) => (
<React.Fragment>
{userIds.map(userId => (
<UserInfo userId={userId} />
))}
</React.Fragment>
);
Data and Service Providers
const UserDataConsumer = ({ children, userId }) => {
const { request, users } = useContext(UserDataContext);
useEffect(() => {
request(userId);
}, [request, userId]);
const user = users[userId];
const props = {
isLoading: !user,
user,
};
return children(props);
};
Data and Service Providers
const useUserData = (userId) => {
const { request, users } = useContext(UserRestfulResourceFetcherContext);
useEffect(() => {
request(userId);
}, [request, userId]);
return users[userId];
};
const UserDataConsumer = ({ children, userId }) => {
const user = useUserData(userId);
const props = {
isLoading: !user,
user,
};
return children(props);
};
Lesson 3
Error Boundaries
Error Boundaries
- Allow your application to recover from otherwise catastrophic errors by isolating the scope that errors can impact
- Only operate during the render phase, but that can be worked around by using setState to throw async errors you specifically want to handle with your error boundary
- Can be set up to automatically log errors to external services, and to render friendly error messages to your users
Lesson 4
Higher-order Components
- Allow you to separate business logic or mixin feature sets from presentational components that might make use of it
- Work in the form of higher-order functions that return new Component classes
- Example: connect function from react-redux
- Potential for prop name collisions if not handled thoughtfully
Higher-order Components
Higher-order Components
import React from 'react';
import SomeService from 'services/some-service';
const withSomeFeature = Component => {
const ComponentWithSomeFeature = class extends React.Component {
static displayName = `SomeFeature(${Component.displayName})`;
state = {
someFeatureResult: null;
}
componentDidMount() {
this._computeSomeFeatureResult();
}
componentDidUpdate() {
this._computeSomeFeatureResult();
}
_computeSomeFeatureResult() {
this.setState({
someFeatureResult: SomeService.getResults(this.props),
});
}
render() {
return <Component someFeature={this.state.someFeatureResult} {...this.props} />;
}
};
return ComponentWithSomeFeature;
}
export default withSomeFeature;
Higher-order Components
import React from 'react';
import withSomeFeature from 'hocs/with-some-feature';
const MyComponent = ({ children, someFeature }) => {
if (!someFeature) {
return null;
}
return (
<div>
<div>Some feature is {someFeature}!</div>
{children}
</div>
};
export default withSomeFeature(MyComponent);
Lesson 5
React Hooks
React Hooks
- Only available for functional components
- Function names must start with the word "use"
- Allow you to abstract useful service-related functionality into simple function calls, and test that functionality in one place
- Highly composable, just like the React components they serve
- May only run during the render cycle, but provide a couple of escape hatches for asynchronous needs
- When used properly, hooks have a very "mixin" feel without the drawbacks of using actual mixin patterns (name collisions, tight coupling, etc)
import React from 'react';
const MyStatefulComponent = class extends React.Component {
_ref = React.createRef();
state = {
isOpen: this.props.isOpen,
};
componentDidMount() {
this.setState({ isOpen: this.props.isOpen });
if (this.props.isOpen) {
document.addEventListener('click', this._documentClick);
}
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.isOpen !== this.props.isOpen) {
this.setState({ isOpen: this.props.isOpen });
}
if (prevState.isOpen !== this.state.isOpen) {
if (this.state.isOpen) {
document.addEventListener('click', this._documentClick);
} else {
document.removeEventListener('click', this._documentClick);
}
}
}
componentWillUnmount() {
if (this.state.isOpen) {
document.removeEventListener('click', this._documentClick);
}
}
_documentClick = (e) => {
if (!this._ref.current.contains(e.target)) {
this.setState({ isOpen: false });
}
};
_onSelect = (e) => {
this.props.onChange(e.target.dataset.value);
this.setState({ isOpen: false });
};
render() {
const {
options,
value,
} = this.props;
return (
<div>
<div className="dropdown-value">{value}</div>
{isOpen && (
<div className="dropdown-list" ref={this._ref}>
{options.map(option => {
<div data-value={option.value} key={option.value} onClick={this._onSelect}>
{option.display}
</div>
})}
</div>
)}
</div>
);
}
}
React Hooks
React Hooks
import React, { useCallback, useEffect, useRef, useState } from 'react';
const MyStatefulFunctionalComponent = ({
isOpen: isOpenProp,
onChange,
options,
value,
}) => {
const ref = useRef();
const [isOpen, setIsOpen] = useState(isOpenProp);
useEffect(() => {
if (isOpen) {
const documentClick = (e) => {
if (!ref.current.contains(e.target)) {
setIsOpen(false);
}
};
document.addEventListener('click', documentClick);
return () => {
document.removeEventListener('click', documentClick);
};
}
}, [isOpen]);
useEffect(() => {
setIsOpen(isOpenProp);
}, [isOpenProp]);
const onSelect = useCallback((e) => {
onChange(e.target.dataset.value);
setIsOpen(false);
}, [onChange]);
return (
<div>
<div className="dropdown-value">{value}</div>
{isOpen && (
<div className="dropdown-list" ref={ref}>
{options.map(option => {
<div data-value={option.value} key={option.value} onClick={onSelect}>
{option.display}
</div>
})}
</div>
)}
</div>
);
};
Lesson 6
React Refs
- The proper way to deal with rendered elements
- Enable you to get a reference to a component instance or an actual DOM element
- Enable you to get a reference to a DOM element
- React.forwardRef for functional components to expose underlying semantic details to consumers
React Refs
Lesson 7
React.Suspense and React.lazy
React.Suspense and React.lazy
- Built-in methods for dealing with asynchronous loading and rendering of components
- React.lazy is built for dynamic imports (code splitting)
- React.Suspense is built to render fallback content while waiting for asynchronous code chunks to load
React.Suspense and React.lazy
import React, { lazy, Suspense } from 'react';
import LoadingSpinner from '../components/loading-spinner';
import Modal from '../components/modal';
import Router from '../router';
import TermsAndServicesLink from '../components/tos-link';
const TermsAndServices = lazy(() => import('../legal/tos'));
const Layout = () => {
const [showTos, setShowTos] = useState(false);
return (
<React.Fragment>
<main>
<Router />
</main>
<footer>
<TermsAndServicesLink onClick={() => setShowTos(true)} />
{showTos && (
<Modal onClose={() => setShowTos(false)}>
<Suspense fallback={<LoadingSpinner />}>
<TermsAndServices />
</Suspense>
</Modal>
)}
</footer>
</div>
);
};
export default Layout;
Concurrent Rendering Mode
Coming Soon!
Concurrent Rendering Mode
- Currently, React renders synchronously, and reconciling large component trees can tie up the main thread, leading to sluggish user interactions and dropped frames
- Time slicing feature will allow the reconciliation cycle to be divided into work units and processed based on priority
- User interactions have highest priority
- State updates can have assigned priority levels to dictate which updates are more important than others
- React may choose to pause, abandon, or restart partially-reconciled component trees based on incoming events and priority settings
Concurrent Rendering Mode
Concurrent Rendering Mode
Concurrent Rendering Mode
fn.
Day 2 complete, congratulations!
Richard Lindsey @Velveeta
Advanced React Workshop
By Richard Lindsey
Advanced React Workshop
- 161