diff --git a/src/server/middlewares/serverRenderer.tsx b/src/server/middlewares/serverRenderer.tsx
index 4c7b450..21daa41 100644
--- a/src/server/middlewares/serverRenderer.tsx
+++ b/src/server/middlewares/serverRenderer.tsx
@@ -1,4 +1,4 @@
-import { HelmetProvider, FilledContext } from 'react-helmet-async'
+import { HelmetProvider, HelmetServerState } from 'react-helmet-async'
import { renderToPipeableStream, renderToString } from 'react-dom/server'
import { Request, Response, RequestHandler } from 'express'
import { StaticRouter } from 'react-router-dom/server'
@@ -61,6 +61,13 @@ const serverRenderer =
const helmetContext = {}
+ /*
+ react-helmet-async forgot to export this interface after migrating to TypeScript in v2+.
+ */
+ interface HelmetDataContext {
+ helmet: HelmetServerState
+ }
+
const jsx = (
@@ -74,7 +81,7 @@ const serverRenderer =
if (IS_RENDER_TO_STREAM) {
await getDataFromTree(jsx)
- const { helmet } = helmetContext as FilledContext
+ const { helmet } = helmetContext as HelmetDataContext
const { header, footer } = getHtmlTemplate({
preloadedState,
@@ -104,7 +111,7 @@ const serverRenderer =
})
} else {
const reactHtml = renderToString(jsx)
- const { helmet } = helmetContext as FilledContext
+ const { helmet } = helmetContext as HelmetDataContext
const { header, footer } = getHtmlTemplate({
preloadedState,
diff --git a/src/server/server.ts b/src/server/server.ts
index 7e0b2ab..4ec6737 100644
--- a/src/server/server.ts
+++ b/src/server/server.ts
@@ -46,16 +46,16 @@ const runServer = (hotReload?: () => RequestHandler[]): void => {
}
if (IS_DEV) {
- (async () => {
+ ;(async () => {
const { hotReload, devMiddlewareInstance } = await import(
'./middlewares/hotReload'
)
devMiddlewareInstance.waitUntilValid(() => {
runServer(hotReload)
})
- })().then(
- () => {}
- ).catch(er => console.log(er))
+ })()
+ .then(() => {})
+ .catch((er) => console.log(er))
} else {
runServer()
}
diff --git a/src/server/template.ts b/src/server/template.ts
index 18547ef..f1150c0 100644
--- a/src/server/template.ts
+++ b/src/server/template.ts
@@ -35,8 +35,8 @@ export const getHtmlTemplate = (props: {
+ props.preloadedState
+ )}
${props.scriptTags}
diff --git a/src/serviceWorker.ts b/src/serviceWorker.ts
index 72fb3ff..cc19e3f 100644
--- a/src/serviceWorker.ts
+++ b/src/serviceWorker.ts
@@ -13,11 +13,11 @@ const registerSW = async (): Promise => {
export const startServiceWorker = (): void => {
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
- (async () => {
+ ;(async () => {
await registerSW()
- })().then(
- () => {}
- ).catch(er => console.log(er))
+ })()
+ .then(() => {})
+ .catch((er) => console.log(er))
})
}
}
diff --git a/src/store/middlewares/index.ts b/src/store/middlewares/index.ts
new file mode 100644
index 0000000..d01b00e
--- /dev/null
+++ b/src/store/middlewares/index.ts
@@ -0,0 +1,2 @@
+export { persistStateToLocalStorage } from './persistStateToLocalStorage'
+export { startAppListening, listenerMiddleware } from './listener'
diff --git a/src/store/middlewares/listener.ts b/src/store/middlewares/listener.ts
new file mode 100644
index 0000000..e6f584f
--- /dev/null
+++ b/src/store/middlewares/listener.ts
@@ -0,0 +1,16 @@
+import { createListenerMiddleware, addListener } from '@reduxjs/toolkit'
+import type { TypedStartListening, TypedAddListener } from '@reduxjs/toolkit'
+
+import type { RootState, AppDispatch } from 'store/store'
+
+export const listenerMiddleware = createListenerMiddleware()
+
+export type AppStartListening = TypedStartListening
+
+export const startAppListening =
+ listenerMiddleware.startListening as AppStartListening
+
+export const addAppListener = addListener as TypedAddListener<
+RootState,
+AppDispatch
+>
diff --git a/src/store/middlewares/persistStateToLocalStorage.ts b/src/store/middlewares/persistStateToLocalStorage.ts
new file mode 100644
index 0000000..4f3d4bb
--- /dev/null
+++ b/src/store/middlewares/persistStateToLocalStorage.ts
@@ -0,0 +1,49 @@
+/*
+ Simple but yet powerful persist middleware.
+ Runs after every event in Redux, so be sure to filter unnecessary slices.
+ (For example, in SSR version first async RTK Query actions are run before
+ you can execute state rehydration. Without filtering API slice, these actions
+ pull server state to local storage rewriting persist data)
+ For a more precise data persisting use listener middlewares.
+ (You can find a typed empty template in listener.ts file)
+*/
+import { Middleware, Action } from 'redux'
+
+import { RootState } from 'store/store'
+import { localStorageAppKey } from 'constants/commonConstants'
+
+const isAction = (action: unknown): action is Action => {
+ return (action as Action).type !== undefined
+}
+
+const persistStateToLocalStorage =
+ (
+ ignoreSlices: Array,
+ ignoreSliceActions: boolean = true
+ ): Middleware<{}, RootState> =>
+ (store) =>
+ (next) =>
+ (action) => {
+ const result = next(action)
+
+ if (
+ isAction(action) &&
+ ignoreSliceActions &&
+ !ignoreSlices.some((el) => action.type.includes(el))
+ ) {
+ localStorage.setItem(
+ localStorageAppKey,
+ JSON.stringify(store.getState(), (key, value) => {
+ if (ignoreSlices.includes(key as keyof RootState)) {
+ return undefined
+ } else {
+ return value
+ }
+ })
+ )
+ }
+
+ return result
+ }
+
+export { persistStateToLocalStorage }
diff --git a/src/store/rootReducer.ts b/src/store/rootReducer.ts
index 1ec846a..66e8c98 100644
--- a/src/store/rootReducer.ts
+++ b/src/store/rootReducer.ts
@@ -1,9 +1,12 @@
-import { combineReducers, Reducer } from '@reduxjs/toolkit'
+import { combineReducers, AnyAction } from '@reduxjs/toolkit'
+
import { counterReducer } from './counter/counterSlice'
import { themeReducer } from './theme/themeSlice'
import { i18nReducer } from 'i18n/i18nSlice'
import { pokemonApi } from 'api'
+import { reduxHydrationAction } from 'constants/commonConstants'
+
export const rootReducer = {
theme: themeReducer,
counter: counterReducer,
@@ -11,6 +14,22 @@ export const rootReducer = {
[pokemonApi.reducerPath]: pokemonApi.reducer
}
-export function createReducer (): Reducer {
- return combineReducers(rootReducer)
+export const appReducer = combineReducers(rootReducer)
+
+export const mainReducer: any = (
+ state: ReturnType,
+ action: AnyAction
+) => {
+ /*
+ Global action for whole state hydration.
+ */
+ if (action?.type === reduxHydrationAction) {
+ const nextState = {
+ ...state,
+ ...action.payload
+ }
+ return nextState
+ }
+
+ return appReducer(state, action)
}
diff --git a/src/store/store.ts b/src/store/store.ts
index cf1cfac..d03a53b 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -12,14 +12,21 @@ import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import { ThunkAction } from 'redux-thunk'
-import { rootReducer } from './rootReducer'
+import { rootReducer, mainReducer } from './rootReducer'
import { pokemonApi } from 'api'
+import { persistStateToLocalStorage } from './middlewares'
+import { isServer } from 'utils'
+
+const middlewares = [
+ ...(!isServer ? [persistStateToLocalStorage(['counter', 'pokemonApi'])] : []),
+ pokemonApi.middleware
+]
const initStore = (preloadedState?: Partial): EnhancedStore =>
configureStore({
- reducer: rootReducer,
+ reducer: mainReducer,
middleware: (getDefaultMiddleware) =>
- getDefaultMiddleware().concat(pokemonApi.middleware),
+ getDefaultMiddleware().concat(middlewares),
preloadedState,
devTools: String(process.env.NODE_ENV).trim() !== 'production'
})
diff --git a/src/style/common.scss b/src/style/common.scss
index 4e5e795..02de501 100644
--- a/src/style/common.scss
+++ b/src/style/common.scss
@@ -6,12 +6,12 @@ body,
}
* {
- color: var(--secondary-color);
font-family: $primary-font;
+ color: var(--secondary-color);
> %headings {
- color: var(--primary-color);
font-family: $secondary-font;
+ color: var(--primary-color);
}
}
@@ -20,6 +20,7 @@ body,
width: 100%;
min-height: 100vh;
+
background: var(--bg-image) no-repeat;
background-attachment: fixed;
background-position: center;
@@ -48,8 +49,9 @@ a:hover {
min-width: 280px;
min-height: 200px;
padding: 30px;
- border-radius: var(--border-radius);
+
background: var(--main-bg-color);
+ border-radius: var(--border-radius);
box-shadow: 0 0 12px 0 rgb(0 0 0 / 22%);
}
diff --git a/src/style/reset.scss b/src/style/reset.scss
index d94d416..96ba8a3 100644
--- a/src/style/reset.scss
+++ b/src/style/reset.scss
@@ -1,5 +1,5 @@
* {
box-sizing: border-box;
- padding: 0;
margin: 0;
+ padding: 0;
}
diff --git a/src/utils/testRedux.tsx b/src/utils/testRedux.tsx
index 6f44001..ef0550f 100644
--- a/src/utils/testRedux.tsx
+++ b/src/utils/testRedux.tsx
@@ -6,7 +6,7 @@ import { Provider } from 'react-redux'
import type { Store, RootState } from 'store/store'
// As a basic setup, import your same slice reducers
-import { rootReducer } from 'store/rootReducer'
+import { mainReducer } from 'store/rootReducer'
// This type interface extends the default options for render from RTL, as well
// as allows the user to specify other things such as initialState, store.
@@ -21,7 +21,7 @@ export function renderWithProviders (
preloadedState = {},
// Automatically create a store instance if no store was passed in
- store = configureStore({ reducer: rootReducer, preloadedState }),
+ store = configureStore({ reducer: mainReducer, preloadedState }),
...renderOptions
}: ExtendedRenderOptions = {}
): ExtendedRenderOptions {
diff --git a/webpack/client.config.ts b/webpack/client.config.ts
index 55beb2a..f7684fb 100644
--- a/webpack/client.config.ts
+++ b/webpack/client.config.ts
@@ -14,6 +14,7 @@ import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'
import HtmlWebpackPlugin from 'html-webpack-plugin'
import CopyPlugin from 'copy-webpack-plugin'
+import 'webpack-dev-server'
import { ALIAS, DEV_SERVER_PORT, DIST_DIR, IS_DEV, IS_LAZY_COMPILATION, SRC_DIR } from './constants'
import * as Loaders from './loaders'