1.0.0 • Published 5 years ago

react-use-feeds v1.0.0

Weekly downloads
2
License
MIT
Repository
github
Last release
5 years ago

React Feeds

React Feeds is a library designed to work with React. Its main purpose is to facilitate the flow of data within a React application, by allowing components to pass on data and/or transform it without the necessity to render. The main advantages of feeds are improved performance, reusability and maintainability of a React application.

Principles

React Feeds is based on a set of core principles. This helps keeping the library’s objective clear and its implementation focused. They are not absolute principles, they can be agreed or disagreed with and rightfully so. If you find yourself aligned with them, this library may turn out useful.

Principle #1

A react component should only render when it needs to change the structure of its children (add/remove a child, change children order, etc).

The following tasks should be achievable without having to re-render the component:

  • Pass a different value to the props of one of the component’s children
  • Receive a different value, transform it and pass it to the props of one of the component’s children

Principle #2

All the external data (data not coming from the component’s internal state) a component and its children consume should be provided by its parent.

Principle #1 by itself could be addressed by functionality like Redux’s connect. Connect however violates Principle #2, hiding the data a component consumes and removing it altogether from the component’s props (as seen from the parent). This leads to a series of problems:

  • Reusability: components that don’t receive all the data they consume through props are hard to reuse. Using the same component in different parts of the application to show different data can become problematic.
  • Consistency: decisions are usually made along the component tree to show, hide or otherwise manipulate children based on the shape of the data (a section of the data missing can result in a child not being rendered). If a component doesn’t receive its data through props, it will need to assume that the data was there when it was rendered by its parent. This leads to weak assumptions and no easy way to guarantee consistency throughout the component tree.
  • Distribution: distributing components that don’t advertise, through props, all the data they consume can be confusing, as the data they need is not clear from their interface alone.

What Is A Feed

A feed is an object with the following interface:

interface Feed<Data> {
    public readonly data: Data
    public subscribe(subscriber: (nextData: Data, previousData: Data) => void): Subscription
}

interface Subscription {
    public unsubscribe()
}

Its purpose is to store a typed value. Anybody can subscribe to the feed and get notified when the value it stores changes.

In practice, you’ll rarely use anything from these interfaces directly. Instead, you have available a set of hooks that allow you to use feeds inside React components respecting the principles mentioned above with as little complexity (aka bugs!) as possible.

Where Should Feeds Be Used

Everywhere!

All props which move information forward should ideally be feeds. Only props moving information backward (e.g. callbacks) shouldn’t be wrapped in a feed.

Thus the following:

interface ButtonProps {
    text: string
    badgesCount: number
    onClick: () => void
}

Becomes:

interface ButtonProps {
    text: Feed<string>
    badgesCount: Feed<number>
    onClick: () => void
}

Basic Usage

Creating a Feed

A feed can be created by using the useFeed hook. This receives the feed’s data as an argument and returns a feed object, which is guaranteed to be the same between invocations. At every subsequent invocation to useFeed, the feed is updated with the provided data and subscribers to that feed are notified. Because the feed object is the same at every invocation, it can be provided to child components without causing them to render (provided they’re a PureComponent or something like React.memo is used).

Accessing a Feed’s Data

When a component is passed a feed object from its parent, it can access the feed’s data by using the useFeedData hook. This receives a feed object as an argument and returns the data stored in the feed. Additionally, it causes the component using the hook to automatically re-render whenever new data is available on the feed.

Transforming a Feed

Sometimes a component receives data through its props, transforms it - for example extracting a portion of it - and passes the transformed data to one or more of its children. When data is wrapped in a feed, this operation can be achieved, without re-rendering the component, with the useFeedTransform hook. useFeedTransform receives a feed and a transformer function as arguments and returns a new transformed feed, which is guaranteed to be the same between invocations. Whenever the original feed receives new data, the transformer function is called with the new data and the returned feed is updated with the transformation result.

Putting everything together

TopComponent.tsx

const TopComponent: React.SFC = () => {
	const [count, setCount] = useState(0)

	const countFeed = useFeed(count)

	const increaseCount = useCallback(() => setCount(count + 1), [count])

	return <MiddleComponent countFeed={countFeed} increaseCount={increaseCount} />
}

MiddleComponent.tsx

export interface Props {
	countFeed: Feed<number>
	increaseCount: () => void
}

export const MiddleComponent: React.SFC<Props> = React.memo(({countFeed, increaseCount}) => {
	const labelFeed = useFeedTransform(countFeed, count => {
		if (count < 3) {
			return “Not there yet!”
		} else if (count < 5) {
			return “Almost there!”
		} else {
			return “There you are!”
		}
	}, [])

	return (
		<div>
			<BottomComponent labelFeed={labelFeed} />
			<button onClick={increaseCount}>Click me</button>
		</div>
	)
})

BottomComponent.tsx

export interface Props {
	labelFeed: Feed<string>
}

export const BottomComponent: React.SFC<Props> = React.memo(({labelFeed}) => {
	const label = useFeedData(labelFeed)

	return (
		<p>{label}</p>
	)
})

In the above example, MiddleComponent only renders once. Any subsequent update to countFeed goes directly to BottomComponent, after being transformed to a label, without MiddleComponent having to re-render.

Advanced Usage

Optional Feed Transform

Transforming a piece of data can go wrong - maybe the portion of the data that the transformation needs to extract is missing or invalid. In this cases we may want to hide the child component that’s supposed to render with the transformed data, or replace it with something else. A special hook is available for this class of scenarios: useOptionalFeedTransform. This behaves the same as useFeedTransform, but with an important difference: it can return either a feed or null:

  • When new data is available on the provided feed and the provided transformer returns successfully, a feed is returned
  • When new data is available on the provided feed and the provided transformer throws an exception, null is returned The returned feed is guaranteed to always stay the same between invocations, even in the scenario where the hook returns a feed, then null, then feed again. Whenever the return value of the hook toggles (between the feed and null), the component using useOptionalFeedTransform is automatically re-rendered so it can change its children structure accordingly.

Here’s an example of how this can be used:

Component.tsx

export interface Props {
	dataFeed: Feed<{ regularData: number, optionalData?: string }>
}

export const Component: React.SFC<Props> = React.memo(({dataFeed}) => {
	const optionalDataFeed: Feed<string> | null = useOptionalFeedTransform(dataFeed, data => {
		const optionalData = data.optionalData
		if (!optionalData) {
			throw new Error("Optional data missing")
		}
		return optionalData
	})

	return (
		<div>
			{ optionalDataFeed && <OptionalDataDisplayer data={optionalDataFeed} /> }
		</div>
	)
})

OptionalDataDisplayer.tsx

export interface Props {
	data: Feed<string>
}

export const OptionalDataDisplayer: React.SFC<Props> = (...)

Grouping Feeds

When you need to group multiple feeds into a single one, so that you can transform it and/or pass it to a child component, useFeedGroup can be used. This hook accepts as an argument a key-value pair where every value is a feed, returning a feed containing an object with data from all the passed feeds. The returned feed is guaranteed to remain the same between invocations. Whenever one of the provided feeds is updated, the returned feed is updated with the latest values from all provided feeds.

For example:

export interface Props {
	valueFeed: Feed<number>
	minFeed: Feed<number>
	maxFeed: Feed<number>
}

export const Component: React.SFC<Props> = React.memo(({ valueFeed, minFeed, maxFeed }) => {
	const isInRangeFeed: Feed<boolean> = useFeedTransform(
		useFeedGroup({ value: valueFeed, min: minFeed, max: maxFeed }), 
		({value, min, max}) => value > min && value < max
	)

	return (
		<RangeIndicator isInRangeFeed={isInRangeFeed} />
	)
})

Enumerating Feeds

useFeedTransform can be used to extract a portion of data and pass it down to children, but falls short when the data is a list of element, each element being passed to an individual child by rendering with map or similar. To help in this scenarios, the useFeedEnumerator hook can be used. This receives a feed whose data is a list of elements and a key provider function. It then returns a list of feeds - each one storing the individual elements - along with the respective keys. Whenever the provided feed is updated, the list is iterated with the help of the key provider function and the individual feeds are updated, without re-rendering the component using the useFeedEnumerator hook. If new elements are present in the list (elements with a key that didn’t exist before), new feeds are created and inserted in the returned list. If elements are removed from the list, the corresponding feeds are removed from the returned array. When new feeds are inserted or old feeds are removed, the component using useFeedEnumerator is re-rendered, so that is can update its children structure.

Here’s an example:

export interface Props {
	todosFeeds: Feed<Array<{ key: string, name: string, value: number }>>
}

export const Component: React.SFC<Props> = React.memo(({ todosFeeds }) => {
	const todoFeedItems = useFeedEnumerator(todosFeeds, todo => todo.key)

	return (
		<div>
			{todoFeedItems.map(feedItem => <Todo key={feedItem.key} data={feedItem.feed} />)}
		</div>
	)
})

Subscribing to a Feed

From within a React component, It is possible to run a piece of code every time a feed is updated by using the useFeedSubscription hook. This will receive a feed and a subscriber function, and will call the subscriber function every time the feed is updated.

Creating a Feed Directly

Feeds don’t have to be created through the useFeed hook necessarily, any object implementing the Feed interface can be used as a feed and work with all of the hooks in this library. This can be useful whenever useFeed cannot be used, for example because the feed is being created outside of a React component or in a scenario that doesn’t respect the Rules of Hooks. For convenience, the BasicFeed type is available. This is a type that implements the Feed interface and exposes an update method, which allows to modify the value stored by the feed and notify all subscribers about the change.

1.0.0

5 years ago