Skip to content

Commit

Permalink
Add example and update Readme (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomaszczura authored Aug 9, 2022
1 parent 994a075 commit a1507ec
Show file tree
Hide file tree
Showing 29 changed files with 27,100 additions and 55 deletions.
239 changes: 188 additions & 51 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,17 @@

- Use your React Context without the additional re-renders in the consumers
- Ability to read values out of context "on-the-fly" - useful in callbacks so you don't have to bind the UI to a context value change just to use the value in a callback
- Redux-like pattern (reducer, actions, and selectors)
- Built with TypeScript

## About
React Context is a comfortable tool to use as a React developer because it comes bundled with React. And it uses a familiar pattern that you as a React develop enjoy. The [downsides](https://blog.thoughtspile.tech/2021/10/04/react-context-dangers/) of React Context are known and this was our approach at Air to solve it. We've looked at [other solutions](https://github.com/dai-shi/use-context-selector) but they've had too many issues/lacked features (like reading values on the fly) so we decided to roll our own.
Here at [Air](https://air.inc), we needed a way to store _multiple_ instances of complex global state (what React Context API does) but with the performance of Redux. `react-memoized-context` solves this problem.

### Why not React Context?
A React Context provider renders _all_ consumers every time it's `value` changes - even if the component isn't using a property on the `value` (if it's an object). This can cause lots of performance issues and the community is [trying](https://github.com/reactjs/rfcs/pull/119) to [solve it](https://github.com/dai-shi/use-context-selector). We've looked at these other solutions but they're either not ready, had too many bugs or lacked features (like reading values on the fly) so we decided to roll our own.

### Why not Redux?
Redux is great as a global store when multiple components want to read and write to a _single_ centralized value. But when you want to have _multiple_ global values with the same structure, Redux isn't as flexible because you need to duplicate your reducers, actions, and selectors. That's where React Context is nice because you can just wrap around another Provider.

## Install

Expand All @@ -26,53 +34,182 @@ yarn add @air/react-memoized-context

## Usage

Create a Context and Provider as usual:

```tsx
interface MyProviderProps {
children: ReactNode;
}

interface MyContextType {
name: string;
age: number;
}

const MyContext = createContext<MyContextType>();

export const MyContextProvider = ({ children }: AnnotationProviderProps) => {

const value: AnnotationContextType = useMemo(
() => ({
...contextValue,
setNewAnnotation,
setActiveAnnotation,
setAnnotationType,
setAnnotationColor,
setAnnotationSize,
clearNewAnnotation,
undo,
setAnnotationsEnabled,
redo,
clearRevertedLines,
addRevertedLine,
}),
[
addRevertedLine,
clearNewAnnotation,
clearRevertedLines,
contextValue,
redo,
setActiveAnnotation,
setAnnotationColor,
setAnnotationSize,
setAnnotationType,
setAnnotationsEnabled,
setNewAnnotation,
undo,
],
);

return <MyContext.Provider value={value}>{children}</MyContext.Provider>;
};
```
1. Create types for your context:

- create type for value, which you want to store:
```typescript
export interface User {
id: string;
name: string;
score: number;
}

export interface UsersTeamContextValue {
users: User[];
}
```
- create type for actions you want to provide to update value:
```typescript
export interface UsersTeamContextActions {
addUser: (user: User) => void;
assignScore: (userId: User['id'], score: number) => void;
}
```
- create type for your context - remember to extend `MemoizedContextType`:
```typescript
export interface UsersTeamContextType extends MemoizedContextType<UsersTeamContextValue>, UsersTeamContextActionsType {}
```
- create default value for your context:
```typescript
export const defaultUsersTeamContextValue: UsersTeamContextType = {
...defaultMemoizedContextValue,
getValue: () => ({
users: [],
}),
addUser: () => {},
assignScore: () => {},
};
```
- create types for your actions - you will use them to modify context value:
```typescript
export interface AddUserAction extends MemoizedContextAction {
type: 'addUser';
data?: { user: User };
}
export interface AssignScoreAction extends MemoizedContextAction {
type: 'assignScore';
data?: { userId: User['id']; score: number };
}
export type UserTeamContextActions = AddUserAction | AssignScoreAction;
```
2. Create your context:

```typescript
const UsersTeamContext = createContext<UsersTeamContextType>(defaultUsersTeamContextValue);
const useUsersTeamContext = () => useContext(UsersTeamContext);
```

3. Create your dispatch method. It should work as redux dispatch - takes an action, modifies state value and returns a new state:
```typescript
export const usersTeamContextDispatch = (state: UsersTeamContextValue, action: UserTeamContextActions) => {
switch (action.type) {
case 'assignScore':
return {
...state,
users: state.users.map((user) => {
if (user.id === action.data?.userId) {
return {
...user,
score: action.data?.score ?? 0,
};
}
return user;
}),
};
case 'addUser':
return {
...state,
users: action.data ? [...state.users, action.data.user] : state.users,
};
}
};
```
4. Create your provider:
```typescript
export const UsersTeamProvider = ({ children }: PropsWithChildren<{}>) => {
const { contextValue } = useMemoizedContextProvider<UsersTeamContextValue>(
// provide default value for your context
{
users: [],
},
usersTeamContextDispatch,
);
// create methods you want to expose to clients
const addUser = useCallback((user: User) => contextValue.dispatch({ type: 'addUser', data: { user } }), [contextValue]);
const assignScore = useCallback(
(userId: User['id'], score: number) => contextValue.dispatch({ type: 'assignScore', data: { userId, score } }),
[contextValue],
);
// memoize your final value that will be available for clients
// just return what's in contextValue and add your methods
const value = useMemo<UsersTeamContextType>(
() => ({
...contextValue,
addUser,
assignScore,
}),
[addUser, assignScore, contextValue],
);
return <UsersTeamContext.Provider value={value}>{children}</UsersTeamContext.Provider>;
};
```

5. To retrieve data from context, you need selectors:
```typescript
export const usersTeamUsersSelector = (state: UsersTeamContextValue) => state.users;
```

usage in component:
```typescript
const context = useUsersTeamContext();
// pass context to useMemoizedContextSelector
const users = useMemoizedContextSelector(context, usersTeamUsersSelector);
```

to simplify it, you can create a helper:
```typescript
export function useUsersTeamContextSelector<T>(selector: (st: UsersTeamContextValue) => T) {
const context = useUsersTeamContext();
return useMemoizedContextSelector(context, selector);
}
```
then, to retrieve `users` from context you can do:

```typescript
const users = useUsersTeamContextSelector(usersTeamUsersSelector);
```

6. Start using your context!

Wrap your components with your `Provider` component, as you do with React Context:

```react
<UsersTeamProvider>
<UsersTeam name="Team 1" />
</UsersTeamProvider>
```

To modify context value, use any of your actions:

```typescript
import { useUsersTeamContextSelector } from "./usersTeamContext";
const { addUser } = useUsersTeamContext()
const onClick = () => {
addUser({ name: 'John' })
}
```

You can read context values on the fly if you need. For example, we will create a user with `users.length` as id. We can use `usersTeamUsersSelector`, but the component would be rerendered every time when any user changes. We don't want that - we need just `users` length. We could create a selector that gets users length, but again - everytime we add a user, the component will rerender. For us, it's enough to know users length by the time we create a user:
```typescript
// get whole context value - it will not cause any rerender!
const contextValue = useUsersTeamContext();
const addNewUser = () => {
// read users array when we need it
const users = contextValue.getValue().users;
// call addUser action to add a new user
contextValue.addUser({ id: users.length + 1, name: userName, score: 0 });
};
```
23 changes: 23 additions & 0 deletions example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*
54 changes: 54 additions & 0 deletions example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Getting Started with Create React App

This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).

## Available Scripts

In the project directory, you can run:

### `yarn start`

Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.

The page will reload if you make edits.\
You will also see any lint errors in the console.

### `yarn test`

Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.

### `yarn build`

Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.

The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!

See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.

### `yarn eject`

**Note: this is a one-way operation. Once you `eject`, you can’t go back!**

If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.

Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.

You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.

## Learn More

You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).

To learn React, check out the [React documentation](https://reactjs.org/).



## Description

This app is an example how to use `useMemoizedContext`. It contains two sections - 'Team 1' and 'Team 2', to which you can add users. For each user you can assign random score.
Each user's row and team section have a small 'Rerenders' text - it is there two show how many times each component rerenders.
See that if you add any user or change user's score, only that row and container rerenders - no unnecessary rerenders of other user's rows (and the other Team)
Loading

0 comments on commit a1507ec

Please sign in to comment.