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 (
-
- )
- }
+ return (
+
+ )
}
-
-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
+}