Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Mobx Stores #4

Closed
wants to merge 55 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
6ef980d
Add Mobx Stores
codeBelt Jan 15, 2021
7e70a62
Add Mobx Stores
codeBelt Jan 15, 2021
d6dbc77
Add Mobx Stores
codeBelt Jan 15, 2021
383c77b
Merge branch 'master' into mobx-stores
codeBelt Jan 15, 2021
680d25c
Add Mobx Stores
codeBelt Jan 15, 2021
0edeba9
Merge branch 'master' into mobx-stores
codeBelt Jan 15, 2021
239b9c1
Add GlobalStoreProvider
codeBelt Jan 17, 2021
ce3f4e1
Add Mobx Stores
codeBelt Jan 18, 2021
d2bd54d
Add Mobx Stores
codeBelt Jan 18, 2021
feded9d
ActorsSortOption
codeBelt Jan 18, 2021
f64b7b9
LocalStoreProvider
codeBelt Jan 18, 2021
793bd3e
displayName
codeBelt Jan 19, 2021
6e90329
Wrap route pages with observer
codeBelt Jan 19, 2021
a673d57
Merge pull request #3 from codeBelt/LocalStoreProvider
codeBelt Jan 19, 2021
938c827
getServerSideProps
codeBelt Jan 19, 2021
0887127
isBrowser
codeBelt Jan 19, 2021
e310a91
comment
codeBelt Jan 19, 2021
79b487e
nprogress
codeBelt Jan 19, 2021
3f0fad2
configure
codeBelt Jan 19, 2021
59cf553
packages
codeBelt Jan 19, 2021
6d4987a
EpisodesToggle
codeBelt Jan 20, 2021
e27d641
NProgress.configure
codeBelt Jan 20, 2021
8e259dc
update packages
codeBelt Jan 20, 2021
bbda068
update readme
codeBelt Jan 20, 2021
43e5342
configure
codeBelt Jan 20, 2021
cf6ab32
clean up
codeBelt Jan 22, 2021
6f05551
Merge branch 'main' into mobx-stores
codeBelt Jan 22, 2021
42bd97b
Merge branch 'master' into mobx-stores
codeBelt Jan 22, 2021
cead138
simplify environments
codeBelt Jan 22, 2021
4ea5c27
update
codeBelt Jan 22, 2021
95dbaf6
update README.md
codeBelt Jan 22, 2021
8419d18
update README.md
codeBelt Jan 22, 2021
90afab3
update README.md
codeBelt Jan 22, 2021
80c3f6a
Update README.md
codeBelt Jan 22, 2021
05c404d
Convert AboutPageStore to be a class an use makeAutoObservable
codeBelt Jan 23, 2021
7de9390
update README.md
codeBelt Jan 23, 2021
196de28
update results
codeBelt Jan 23, 2021
78f3a6a
Merge branch 'main' into mobx-stores
codeBelt Jan 23, 2021
5d91f55
clean types
codeBelt Jan 23, 2021
a9cf948
Merge branch 'main' into mobx-stores
codeBelt Jan 23, 2021
b3d9515
remove runInAction from enqueueToast
codeBelt Jan 23, 2021
c5cc95c
Convert Stores to use Classes
codeBelt Jan 23, 2021
26ef63c
update README.md
codeBelt Jan 24, 2021
b4d77d8
Routes.constants.ts
codeBelt Jan 25, 2021
1a7d8db
Update packages
codeBelt Jan 28, 2021
3b5c7ab
Add error toast to Auth
codeBelt Jan 28, 2021
8efaad4
Merge branch 'main' into mobx-stores
codeBelt Jan 28, 2021
586fd97
Merge branch 'main' into es6-classes
codeBelt Jan 28, 2021
38cd7c1
fix import path
codeBelt Jan 28, 2021
ead4e6c
Merge branch 'master' into mobx-stores
codeBelt Jan 28, 2021
a6c9e40
Convert AboutPageStore to factory function store
codeBelt Feb 26, 2021
b88952d
Update types on stores
codeBelt Feb 26, 2021
22b959d
Merge pull request #5 from codeBelt/es6-classes
codeBelt Feb 26, 2021
ad45800
update readme
codeBelt Feb 26, 2021
83a30f0
Merge branch 'main' into mobx-stores
codeBelt Feb 26, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ I will be writing an article on the follow code example, but I wanted to share t

## Global & Local State with MobX React Example

### A factory function approach to MobX stores with React & Next.js
### MobX stores with React & Next.js

[View the demo site](https://mobx-local-global-stores.vercel.app/)

Expand All @@ -15,33 +15,31 @@ I will be writing an article on the follow code example, but I wanted to share t
#### [GlobalStores](https://github.com/codeBelt/mobx-local-global-stores/blob/main/src/stores/GlobalStore.ts)

- **[AuthGlobalStore](https://github.com/codeBelt/mobx-local-global-stores/blob/main/src/stores/auth/AuthGlobalStore.ts)**:
- Factory function store that uses `observable`
- ES6 Class store that uses `makeAutoObservable`
- A fake auth store to keep track of a user loaded from [randomuser.me](https://randomuser.me/)
- **[ToastGlobalStore](https://github.com/codeBelt/mobx-local-global-stores/blob/main/src/stores/toast/ToastGlobalStore.ts)**:
- Factory function store that uses `observable`
- ES6 Class store that uses `makeAutoObservable`
- A store that uses [notistack](https://iamhosseindhv.com/notistack) to create a notification (toast) system

#### Local Stores

- **[IndexPageStore](https://github.com/codeBelt/mobx-local-global-stores/blob/main/src/components/pages/index-page/IndexPage.store.ts)**:
- Factory function store that uses `observable`
- ES6 Class store that uses `makeAutoObservable`
- Makes two requests in the browser to [api.tvmaze.com](https://api.tvmaze.com)
- Shows a loading indicator and allows the user to sort actors
- **[EpisodesPageStore](https://github.com/codeBelt/mobx-local-global-stores/blob/main/src/components/pages/episodes-page/EpisodesPage.store.ts)**:
- Factory function store that uses `observable`
- ES6 Class store that uses `makeAutoObservable`
- Hydrated with data that was fetched server-side
- Allows the user to sort the data on the page that was previously rendered server-side
- Shows how use the [GlobalStores](https://github.com/codeBelt/mobx-local-global-stores/blob/main/src/stores/GlobalStore.ts) ([getGlobalStore()](https://github.com/codeBelt/mobx-local-global-stores/blob/main/src/components/shared/global-store-provider/GlobalStoreProvider.tsx#L13)) within local stores
- **[AboutPageStore](https://github.com/codeBelt/mobx-local-global-stores/blob/main/src/components/pages/about-page/AboutPage.store.ts)**:
- ES6 Class store that uses `makeAutoObservable` for comparison
- Factory function store that uses `observable` for comparison
- A simple store that makes a request to [httpstat.us](https://httpstat.us)
- Shows how to handle api errors in the local store and on the page
- Shows how use the [GlobalStores](https://github.com/codeBelt/mobx-local-global-stores/blob/main/src/stores/GlobalStore.ts) ([getGlobalStore()](https://github.com/codeBelt/mobx-local-global-stores/blob/main/src/components/shared/global-store-provider/GlobalStoreProvider.tsx#L13)) within local stores

If you want to see an overview of the files using Mobx check out this [PR Diff](https://github.com/codeBelt/mobx-local-global-stores/pull/4/files).

If you want to check out the example with stores using all ES6 Classes [here is the branch](https://github.com/codeBelt/mobx-local-global-stores/tree/es6-classes). ([PR Diff](https://github.com/codeBelt/mobx-local-global-stores/pull/5/files))

### Feedback Welcomed

If you want to give me feedback on how to improve my usage with MobX, TypeScript types, or Next.js. Create an issue on my [repo](https://github.com/codeBelt/mobx-local-global-stores).
Expand All @@ -50,5 +48,4 @@ You could also fork my repo and create a PR to show me how you would improve som

### Help Wanted

- Validate I am following best practice with Mobx and factory functions.
- Help me understand how to remove `runInAction` from [ToastGlobalStore](https://github.com/codeBelt/mobx-local-global-stores/blob/main/src/stores/toast/ToastGlobalStore.ts) and validate I set up [ToastNotifier](https://github.com/codeBelt/mobx-local-global-stores/blob/main/src/components/ui/toast-notifier/ToastNotifier.tsx) correctly.
Binary file added animation.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 36 additions & 0 deletions src/components/pages/about-page/AboutPage.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { observable } from 'mobx';
import { getErrorRequest } from '../../../domains/shows/shows.services';
import { initialResponseStatus } from '../../../utils/mobx.utils';
import { ApiResponse } from '../../../utils/http/http.types';
import { getGlobalStore } from '../../shared/global-store-provider/GlobalStoreProvider';

export const AboutPageStore = () =>
observable({
globalStore: getGlobalStore(),
errorExampleResults: initialResponseStatus<null>(null),

/**
* Store initializer. Should only be called once.
*/
*init() {
yield Promise.all([this.loadSomething()]);
},

*loadSomething() {
const response: ApiResponse<null> = yield getErrorRequest();

this.errorExampleResults = {
data: this.errorExampleResults.data,
isRequesting: false,
...response, // Overwrites the default data prop or adds an error. Also adds the statusCode.
};

if (response.error) {
const message = `${response.statusCode}: ${response.error.message}`;

this.globalStore.toastStore.enqueueToast(message, 'error');
}
},
});

export type AboutPageStore = ReturnType<typeof AboutPageStore>;
31 changes: 31 additions & 0 deletions src/components/pages/about-page/AboutPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import { LoadingIndicator } from '../../ui/loading-indicator/LoadingIndicator';
import { Container, Header, Message } from 'semantic-ui-react';
import { AboutPageStore } from './AboutPage.store';
import { observer } from 'mobx-react-lite';
import { useLocalStore } from '../../shared/local-store-provider/LocalStoreProvider';

interface IProps {}

export const AboutPage: React.FC<IProps> = observer((props) => {
const localStore = useLocalStore<AboutPageStore>();

return (
<div>
<Header as="h2">About</Header>
<LoadingIndicator isActive={localStore.errorExampleResults.isRequesting}>
<Container>
<p>This page is only to show how to handle API errors on the page.</p>
<p>You will also notice a popup indicator with the actual error text.</p>
<p>Below we create a custom error message.</p>
</Container>
{Boolean(localStore.errorExampleResults.error) && (
<Message info={true} header="Error" content="Sorry there was an error requesting this content." />
)}
</LoadingIndicator>
</div>
);
});

AboutPage.displayName = 'AboutPage';
AboutPage.defaultProps = {};
50 changes: 50 additions & 0 deletions src/components/pages/episodes-page/EpisodesPage.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { makeAutoObservable } from 'mobx';
import groupBy from 'lodash.groupby';
import orderBy from 'lodash.orderby';
import { IEpisode, IEpisodeTable } from '../../../domains/shows/shows.types';
import dayjs from 'dayjs';
import { ApiResponse } from '../../../utils/http/http.types';
import { getGlobalStore } from '../../shared/global-store-provider/GlobalStoreProvider';
import { EpisodesToggleOption } from './episodes-toggle/EpisodesToggle.constants';

export class EpisodesPageStore {
readonly globalStore = getGlobalStore();
sortType = EpisodesToggleOption.ASC;
episodesResults: ApiResponse<IEpisode[]>;

constructor(episodesResults: ApiResponse<IEpisode[]>) {
this.episodesResults = episodesResults;

makeAutoObservable(this);
}

get sortedTableData(): IEpisodeTable[] {
return orderBy(this.generateTableData, 'title', this.sortType);
}

get generateTableData(): IEpisodeTable[] {
if (this.episodesResults.error) {
return [];
}

const seasons: { [season: string]: IEpisode[] } = groupBy(this.episodesResults.data, 'season');

return Object.entries(seasons).map(([season, models]) => {
return {
title: `Season ${season}`,
rows: models.map((model) => ({
episode: model.number,
name: model.name,
date: dayjs(model.airdate).format('MMM D, YYYY'),
image: model.image?.medium ?? '',
})),
};
});
}

setSortType(sortType: EpisodesToggleOption): void {
this.sortType = sortType;

this.globalStore.toastStore.enqueueToast('Nice! You just sorted Server-Side Rendered Content.', 'info');
}
}
27 changes: 27 additions & 0 deletions src/components/pages/episodes-page/EpisodesPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { EpisodesPageStore } from './EpisodesPage.store';
import { EpisodesTable } from './episodes-table/EpisodesTable';
import { useLocalStore } from '../../shared/local-store-provider/LocalStoreProvider';
import { observer } from 'mobx-react-lite';
import { Container } from 'semantic-ui-react';
import { EpisodesToggle } from './episodes-toggle/EpisodesToggle';

export interface IProps {}

export const EpisodesPage: React.FC<IProps> = observer((props) => {
const localStore = useLocalStore<EpisodesPageStore>();

return (
<>
<Container textAlign="right" fluid={true}>
<EpisodesToggle />
</Container>
{localStore.sortedTableData.map((model) => (
<EpisodesTable key={model.title} tableData={model} />
))}
</>
);
});

EpisodesPage.displayName = 'EpisodesPage';
EpisodesPage.defaultProps = {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';
import { Header, Table } from 'semantic-ui-react';
import { IEpisodeTable } from '../../../../domains/shows/shows.types';
import { EpisodesTableRow } from './episodes-table-row/EpisodesTableRow';

interface IProps {
readonly tableData: IEpisodeTable;
}

export const EpisodesTable: React.FC<IProps> = (props) => {
return (
<div key={props.tableData.title}>
<Header as="h2">{props.tableData.title}</Header>
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell width={1}>Scene</Table.HeaderCell>
<Table.HeaderCell>Episode</Table.HeaderCell>
<Table.HeaderCell>Date</Table.HeaderCell>
<Table.HeaderCell>Name</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{props.tableData.rows.map((model) => (
<EpisodesTableRow key={model.episode} rowData={model} />
))}
</Table.Body>
</Table>
</div>
);
};

EpisodesTable.displayName = 'EpisodesTable';
EpisodesTable.defaultProps = {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import { Image, Table } from 'semantic-ui-react';
import { IEpisodeTableRow } from '../../../../../domains/shows/shows.types';

interface IProps {
readonly rowData: IEpisodeTableRow;
}

export const EpisodesTableRow: React.FC<IProps> = (props) => {
return (
<Table.Row key={props.rowData.episode}>
<Table.Cell>
<Image src={props.rowData.image} rounded={true} size="small" />
</Table.Cell>
<Table.Cell>{props.rowData.episode}</Table.Cell>
<Table.Cell>{props.rowData.date}</Table.Cell>
<Table.Cell>{props.rowData.name}</Table.Cell>
</Table.Row>
);
};

EpisodesTableRow.displayName = 'EpisodesTableRow';
EpisodesTableRow.defaultProps = {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum EpisodesToggleOption {
ASC = 'asc',
DESC = 'desc',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import { Button } from 'semantic-ui-react';
import { useLocalStore } from '../../../shared/local-store-provider/LocalStoreProvider';
import { EpisodesPageStore } from '../EpisodesPage.store';
import { EpisodesToggleOption } from './EpisodesToggle.constants';
import { observer } from 'mobx-react-lite';

export interface IProps {}

export const EpisodesToggle: React.FC<IProps> = observer((props) => {
const localStore = useLocalStore<EpisodesPageStore>();

return (
<Button.Group>
<Button
positive={localStore.sortType === EpisodesToggleOption.ASC}
onClick={() => localStore.setSortType(EpisodesToggleOption.ASC)}
>
Seasons Ascending
</Button>
<Button.Or />
<Button
positive={localStore.sortType === EpisodesToggleOption.DESC}
onClick={() => localStore.setSortType(EpisodesToggleOption.DESC)}
>
Seasons Descending
</Button>
</Button.Group>
);
});

EpisodesToggle.displayName = 'EpisodesToggle';
EpisodesToggle.defaultProps = {};
58 changes: 58 additions & 0 deletions src/components/pages/index-page/IndexPage.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { makeAutoObservable } from 'mobx';
import { getCastsRequest, getShowRequest } from '../../../domains/shows/shows.services';
import { initialResponseStatus } from '../../../utils/mobx.utils';
import { ICast, IShow } from '../../../domains/shows/shows.types';
import { ApiResponse } from '../../../utils/http/http.types';
import { defaultShowId } from '../../../domains/shows/shows.constants';
import orderBy from 'lodash.orderby';
import { getGlobalStore } from '../../shared/global-store-provider/GlobalStoreProvider';

export class IndexPageStore {
readonly globalStore = getGlobalStore();
sortValue = '';
showResults = initialResponseStatus<IShow | null>(null);
castsResults = initialResponseStatus<ICast[]>([]);

constructor() {
makeAutoObservable(this);
}

get isRequesting(): boolean {
return [this.showResults.isRequesting, this.castsResults.isRequesting].some(Boolean);
}

get actors(): ICast[] {
return orderBy(this.castsResults.data, (cast) => cast.person[this.sortValue], 'asc');
}

setSortOption(sortValue: string) {
this.sortValue = sortValue;
}

/**
* Store initializer. Should only be called once.
*/
*init() {
yield Promise.all([this.loadShow(), this.loadCasts()]);
}

*loadShow() {
const response: ApiResponse<IShow> = yield getShowRequest(defaultShowId);

this.showResults = {
data: this.showResults.data,
isRequesting: false,
...response, // Overwrites the default data prop or adds an error. Also adds the statusCode.
};
}

*loadCasts() {
const response: ApiResponse<ICast[]> = yield getCastsRequest(defaultShowId);

this.castsResults = {
data: this.castsResults.data,
isRequesting: false,
...response, // Overwrites the default data prop or adds an error. Also adds the statusCode.
};
}
}
29 changes: 29 additions & 0 deletions src/components/pages/index-page/IndexPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import { Divider, Header, Icon } from 'semantic-ui-react';
import { LoadingIndicator } from '../../ui/loading-indicator/LoadingIndicator';
import { MainOverview } from './main-overview/MainOverview';
import { Actors } from './actors/Actors';
import { observer } from 'mobx-react-lite';
import { IndexPageStore } from './IndexPage.store';
import { useLocalStore } from '../../shared/local-store-provider/LocalStoreProvider';

interface IProps {}

export const IndexPage: React.FC<IProps> = observer((props) => {
const localStore = useLocalStore<IndexPageStore>();

return (
<LoadingIndicator isActive={localStore.isRequesting}>
<MainOverview />
<Divider horizontal={true}>
<Header as="h4">
<Icon name="users" /> Actors
</Header>
</Divider>
<Actors />
</LoadingIndicator>
);
});

IndexPage.displayName = 'IndexPage';
IndexPage.defaultProps = {};
Loading