diff --git a/examples/with-redux-observable/README.md b/examples/with-redux-observable/README.md index aa537f81fabc8..8f4ce53dbe2ac 100644 --- a/examples/with-redux-observable/README.md +++ b/examples/with-redux-observable/README.md @@ -47,25 +47,6 @@ yarn dev Deploy it to the cloud with [Vercel](https://vercel.com/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). -### Notes - -The main problem with integrating Redux, Redux-Observable and Next.js is -probably making initial requests on a server. That's because, the -`getInitialProps` hook runs on the server-side before epics have been made available by just dispatching actions. - -However, we can access and execute epics directly. In order to do so, we need to -pass them an Observable of an action together with StateObservable and they will return an Observable: - -```js -static async getInitialProps({ store, isServer }) { - const state$ = new StateObservable(new Subject(), store.getState()); - const resultAction = await rootEpic( - of(actions.fetchCharacter(isServer)), - state$ - ).toPromise(); // we need to convert Observable to Promise - store.dispatch(resultAction)}; -``` - Note: we are not using `AjaxObservable` from the `rxjs` library; as of rxjs v5.5.6, it will not work on both the server- and client-side. Instead we call the default export from diff --git a/examples/with-redux-observable/components/CharacterInfo.js b/examples/with-redux-observable/components/CharacterInfo.js deleted file mode 100644 index e2ead714e9e29..0000000000000 --- a/examples/with-redux-observable/components/CharacterInfo.js +++ /dev/null @@ -1,44 +0,0 @@ -import { connect } from 'react-redux' - -const CharacterInfo = ({ - character, - error, - fetchCharacter, - isFetchedOnServer = false, -}) => ( -
- {error ? ( -

We encountered and error.

- ) : ( -
-

Character: {character.name}

-

birth year: {character.birth_year}

-

gender: {character.gender}

-

skin color: {character.skin_color}

-

eye color: {character.eye_color}

-
- )} -

- (was character fetched on server? - {isFetchedOnServer.toString()}) -

- -
-) - -export default connect((state) => ({ - character: state.character, - error: state.error, - isFetchedOnServer: state.isFetchedOnServer, -}))(CharacterInfo) diff --git a/examples/with-redux-observable/components/UserInfo.js b/examples/with-redux-observable/components/UserInfo.js new file mode 100644 index 0000000000000..e3fc0428225ca --- /dev/null +++ b/examples/with-redux-observable/components/UserInfo.js @@ -0,0 +1,50 @@ +import { useSelector } from 'react-redux' + +const useUser = () => { + return useSelector((state) => ({ + character: state.character, + error: state.error, + isFetchedOnServer: state.isFetchedOnServer, + })) +} + +const UserInfo = () => { + const { character, isFetchedOnServer, error } = useUser() + const { name, id, username, email, phone, website } = character + + return ( +
+ {error ? ( +

We encountered and error.

+ ) : ( +
+

Name: {name}

+

Id: {id}

+

Username: {username}

+

Email: {email}

+

Phone: {phone}

+

Website: {website}

+
+ )} +

+ (was user fetched on server? - {isFetchedOnServer.toString()}) +

+

Please note there are no more than 10 users in the API!

+ +
+ ) +} + +export default UserInfo diff --git a/examples/with-redux-observable/demo.png b/examples/with-redux-observable/demo.png deleted file mode 100644 index 2fee551120ec3..0000000000000 Binary files a/examples/with-redux-observable/demo.png and /dev/null differ diff --git a/examples/with-redux-observable/package.json b/examples/with-redux-observable/package.json index 70b4b6fcaf811..5bc377ae4759e 100644 --- a/examples/with-redux-observable/package.json +++ b/examples/with-redux-observable/package.json @@ -6,18 +6,16 @@ "build": "next build", "start": "next start" }, - "author": "tomaszmularczyk(tomasz.mularczyk89@gmail.com)", "dependencies": { "next": "latest", - "next-redux-wrapper": "^2.0.0-beta.6", - "react": "^16.7.0", - "react-dom": "^16.7.0", - "react-redux": "^5.0.7", - "redux": "^4.0.0", - "redux-logger": "^3.0.6", - "redux-observable": "^1.0.0", - "rxjs": "^6.3.3", - "universal-rxjs-ajax": "^2.0.0" + "react": "16.13.1", + "react-dom": "16.13.1", + "react-redux": "7.2.0", + "redux": "4.0.5", + "redux-logger": "3.0.6", + "redux-observable": "1.2.0", + "rxjs": "6.5.5", + "universal-rxjs-ajax": "2.0.4" }, "license": "ISC" } diff --git a/examples/with-redux-observable/pages/_app.js b/examples/with-redux-observable/pages/_app.js index 4bbd0dbdf84ce..7c9abfdb7a49c 100644 --- a/examples/with-redux-observable/pages/_app.js +++ b/examples/with-redux-observable/pages/_app.js @@ -1,25 +1,12 @@ import { Provider } from 'react-redux' -import App from 'next/app' -import withRedux from 'next-redux-wrapper' -import makeStore from '../redux' +import { useStore } from '../store/store' -class MyApp extends App { - static async getInitialProps({ Component, ctx }) { - const pageProps = Component.getInitialProps - ? await Component.getInitialProps(ctx) - : {} +export default function App({ Component, pageProps }) { + const store = useStore(pageProps.initialReduxState) - return { pageProps } - } - - render() { - const { Component, pageProps, store } = this.props - return ( - - - - ) - } + return ( + + + + ) } - -export default withRedux(makeStore)(MyApp) diff --git a/examples/with-redux-observable/pages/index.js b/examples/with-redux-observable/pages/index.js index 6bab7cbfcd639..fbcf6544e671e 100644 --- a/examples/with-redux-observable/pages/index.js +++ b/examples/with-redux-observable/pages/index.js @@ -1,49 +1,30 @@ -import { Component } from 'react' +import { useEffect } from 'react' +import { useDispatch } from 'react-redux' import Link from 'next/link' -import { of, Subject } from 'rxjs' -import { StateObservable } from 'redux-observable' -import { connect } from 'react-redux' -import CharacterInfo from '../components/CharacterInfo' -import { rootEpic } from '../redux/epics' -import * as actions from '../redux/actions' +import UserInfo from '../components/UserInfo' +import { stopFetchingUsers, startFetchingUsers } from '../store/actions' -class Counter extends Component { - static async getInitialProps({ store, isServer }) { - const state$ = new StateObservable(new Subject(), store.getState()) - const resultAction = await rootEpic( - of(actions.fetchCharacter(isServer)), - state$ - ).toPromise() // we need to convert Observable to Promise - store.dispatch(resultAction) +const Counter = () => { + const dispatch = useDispatch() - return { isServer } - } + useEffect(() => { + dispatch(startFetchingUsers()) + return () => { + dispatch(stopFetchingUsers()) + } + }, [dispatch]) - componentDidMount() { - this.props.startFetchingCharacters() - } - - componentWillUnmount() { - this.props.stopFetchingCharacters() - } - - render() { - return ( -
-

Index Page

- -
- -
- ) - } + return ( +
+

Index Page

+ +
+ +
+ ) } - -export default connect(null, { - startFetchingCharacters: actions.startFetchingCharacters, - stopFetchingCharacters: actions.stopFetchingCharacters, -})(Counter) +export default Counter diff --git a/examples/with-redux-observable/redux/actionTypes.js b/examples/with-redux-observable/redux/actionTypes.js deleted file mode 100644 index e42a45c8fa3e2..0000000000000 --- a/examples/with-redux-observable/redux/actionTypes.js +++ /dev/null @@ -1,5 +0,0 @@ -export const FETCH_CHARACTER = 'FETCH_CHARACTER' -export const FETCH_CHARACTER_SUCCESS = 'FETCH_CHARACTER_SUCCESS' -export const FETCH_CHARACTER_FAILURE = 'FETCH_CHARACTER_FAILURE' -export const START_FETCHING_CHARACTERS = 'START_FETCHING_CHARACTERS' -export const STOP_FETCHING_CHARACTERS = 'STOP_FETCHING_CHARACTERS' diff --git a/examples/with-redux-observable/redux/actions.js b/examples/with-redux-observable/redux/actions.js deleted file mode 100644 index 02ca996e73b18..0000000000000 --- a/examples/with-redux-observable/redux/actions.js +++ /dev/null @@ -1,21 +0,0 @@ -import * as types from './actionTypes' - -export const startFetchingCharacters = () => ({ - type: types.START_FETCHING_CHARACTERS, -}) -export const stopFetchingCharacters = () => ({ - type: types.STOP_FETCHING_CHARACTERS, -}) -export const fetchCharacter = (isServer = false) => ({ - type: types.FETCH_CHARACTER, - payload: { isServer }, -}) -export const fetchCharacterSuccess = (response, isServer) => ({ - type: types.FETCH_CHARACTER_SUCCESS, - payload: { response, isServer }, -}) - -export const fetchCharacterFailure = (error, isServer) => ({ - type: types.FETCH_CHARACTER_FAILURE, - payload: { error, isServer }, -}) diff --git a/examples/with-redux-observable/redux/index.js b/examples/with-redux-observable/redux/index.js deleted file mode 100644 index 804ac1b1685a9..0000000000000 --- a/examples/with-redux-observable/redux/index.js +++ /dev/null @@ -1,16 +0,0 @@ -import { createStore, applyMiddleware } from 'redux' -import { createLogger } from 'redux-logger' -import { createEpicMiddleware } from 'redux-observable' -import reducer from './reducer' -import { rootEpic } from './epics' - -export default function initStore(initialState) { - const epicMiddleware = createEpicMiddleware() - const logger = createLogger({ collapsed: true }) // log every action to see what's happening behind the scenes. - const reduxMiddleware = applyMiddleware(epicMiddleware, logger) - - const store = createStore(reducer, initialState, reduxMiddleware) - epicMiddleware.run(rootEpic) - - return store -} diff --git a/examples/with-redux-observable/redux/reducer.js b/examples/with-redux-observable/redux/reducer.js deleted file mode 100644 index 20ba349cd0754..0000000000000 --- a/examples/with-redux-observable/redux/reducer.js +++ /dev/null @@ -1,28 +0,0 @@ -import * as types from './actionTypes' - -const INITIAL_STATE = { - nextCharacterId: 1, - character: {}, - isFetchedOnServer: false, - error: null, -} - -export default function reducer(state = INITIAL_STATE, { type, payload }) { - switch (type) { - case types.FETCH_CHARACTER_SUCCESS: - return { - ...state, - character: payload.response, - isFetchedOnServer: payload.isServer, - nextCharacterId: state.nextCharacterId + 1, - } - case types.FETCH_CHARACTER_FAILURE: - return { - ...state, - error: payload.error, - isFetchedOnServer: payload.isServer, - } - default: - return state - } -} diff --git a/examples/with-redux-observable/store/actionTypes.js b/examples/with-redux-observable/store/actionTypes.js new file mode 100644 index 0000000000000..c089a96ea36eb --- /dev/null +++ b/examples/with-redux-observable/store/actionTypes.js @@ -0,0 +1,5 @@ +export const FETCH_USER = 'FETCH_USER' +export const FETCH_USER_SUCCESS = 'FETCH_USER_SUCCESS' +export const FETCH_USER_FAILURE = 'FETCH_USER_FAILURE' +export const START_FETCHING_USERS = 'START_FETCHING_USERS' +export const STOP_FETCHING_USERS = 'STOP_FETCHING_USERS' diff --git a/examples/with-redux-observable/store/actions.js b/examples/with-redux-observable/store/actions.js new file mode 100644 index 0000000000000..33714f2eaebe6 --- /dev/null +++ b/examples/with-redux-observable/store/actions.js @@ -0,0 +1,21 @@ +import * as types from './actionTypes' + +export const startFetchingUsers = () => ({ + type: types.START_FETCHING_USERS, +}) +export const stopFetchingUsers = () => ({ + type: types.STOP_FETCHING_USERS, +}) +export const fetchUser = (isServer = false) => ({ + type: types.FETCH_USER, + payload: { isServer }, +}) +export const fetchUserSuccess = (response, isServer) => ({ + type: types.FETCH_USER_SUCCESS, + payload: { response, isServer }, +}) + +export const fetchUserFailure = (error, isServer) => ({ + type: types.FETCH_USER_FAILURE, + payload: { error, isServer }, +}) diff --git a/examples/with-redux-observable/redux/epics.js b/examples/with-redux-observable/store/epics.js similarity index 54% rename from examples/with-redux-observable/redux/epics.js rename to examples/with-redux-observable/store/epics.js index 7603e638872f2..191490cfa8d6b 100644 --- a/examples/with-redux-observable/redux/epics.js +++ b/examples/with-redux-observable/store/epics.js @@ -6,38 +6,32 @@ import { request } from 'universal-rxjs-ajax' // because standard AjaxObservable import * as actions from './actions' import * as types from './actionTypes' -export const fetchUserEpic = (action$, state$) => +export const fetchUsersEpic = (action$, state$) => action$.pipe( - ofType(types.START_FETCHING_CHARACTERS), + ofType(types.START_FETCHING_USERS), mergeMap((action) => { - return interval(3000).pipe( - map((x) => actions.fetchCharacter()), + return interval(5000).pipe( + map((x) => actions.fetchUser()), takeUntil( - action$.ofType( - types.STOP_FETCHING_CHARACTERS, - types.FETCH_CHARACTER_FAILURE - ) + action$.ofType(types.STOP_FETCHING_USERS, types.FETCH_USER_FAILURE) ) ) }) ) -export const fetchCharacterEpic = (action$, state$) => +export const fetchUserEpic = (action$, state$) => action$.pipe( - ofType(types.FETCH_CHARACTER), + ofType(types.FETCH_USER), mergeMap((action) => request({ - url: `https://swapi.co/api/people/${state$.value.nextCharacterId}`, + url: `https://jsonplaceholder.typicode.com/users/${state$.value.nextUserId}`, }).pipe( map((response) => - actions.fetchCharacterSuccess( - response.response, - action.payload.isServer - ) + actions.fetchUserSuccess(response.response, action.payload.isServer) ), catchError((error) => of( - actions.fetchCharacterFailure( + actions.fetchUserFailure( error.xhr.response, action.payload.isServer ) @@ -47,4 +41,4 @@ export const fetchCharacterEpic = (action$, state$) => ) ) -export const rootEpic = combineEpics(fetchUserEpic, fetchCharacterEpic) +export const rootEpic = combineEpics(fetchUsersEpic, fetchUserEpic) diff --git a/examples/with-redux-observable/store/store.js b/examples/with-redux-observable/store/store.js new file mode 100644 index 0000000000000..bb741b565a520 --- /dev/null +++ b/examples/with-redux-observable/store/store.js @@ -0,0 +1,73 @@ +import { useMemo } from 'react' +import { createStore, applyMiddleware } from 'redux' +import { createLogger } from 'redux-logger' +import { createEpicMiddleware } from 'redux-observable' +import { rootEpic } from './epics' +import * as types from './actionTypes' + +let store + +const INITIAL_STATE = { + nextUserId: 1, + character: {}, + isFetchedOnServer: false, + error: null, +} + +function reducer(state = INITIAL_STATE, { type, payload }) { + switch (type) { + case types.FETCH_USER_SUCCESS: + return { + ...state, + character: payload.response, + isFetchedOnServer: payload.isServer, + nextUserId: state.nextUserId + 1, + } + case types.FETCH_USER_FAILURE: + return { + ...state, + error: payload.error, + isFetchedOnServer: payload.isServer, + } + default: + return state + } +} + +const initStore = (initialState) => { + const epicMiddleware = createEpicMiddleware() + const logger = createLogger({ collapsed: true }) // log every action to see what's happening behind the scenes. + const reduxMiddleware = applyMiddleware(epicMiddleware, logger) + + const store = createStore(reducer, initialState, reduxMiddleware) + epicMiddleware.run(rootEpic) + + return store +} + +export const initializeStore = (preloadedState) => { + let _store = store ?? initStore(preloadedState) + + // After navigating to a page with an initial Redux state, merge that state + // with the current state in the store, and create a new store + if (preloadedState && store) { + _store = initStore({ + ...store.getState(), + ...preloadedState, + }) + // Reset the current store + store = undefined + } + + // For SSG and SSR always create a new store + if (typeof window === 'undefined') return _store + // Create the store once in the client + if (!store) store = _store + + return _store +} + +export function useStore(initialState) { + const store = useMemo(() => initializeStore(initialState), [initialState]) + return store +}