Improved loading content of and article with saving them in redux store

When article was loaded once than it will be cached in redux store.
On the next loading of article content it will be get from store
This commit is contained in:
Daniel Weissmall 2022-11-12 23:41:31 +03:00
parent 335218fdd9
commit fecce76fe1
13 changed files with 173 additions and 106 deletions

View File

@ -1,30 +1,43 @@
# Install dependencies only when needed
FROM node:fermium-alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
# Install dependencies of project
FROM node:fermium-alpine AS dependencies
RUN apk add --no-cache libc6-compat
WORKDIR /home/app/
COPY package.json package-lock.json ./
RUN npm ci
COPY package.json ./
RUN npm i
# Bundle static assets with nginx
FROM node:fermium-alpine as production
# Copy built assets from builder
# Build application to bunch of static files
FROM node:fermium-alpine AS builder
WORKDIR /home/app/
COPY --from=deps ./home/app/node_modules ./node_modules
COPY --from=dependencies ./home/app/node_modules ./node_modules
COPY . .
RUN npm run build
# NGINX image
FROM nginx:1.21.6 as production
# Copy built assets from builder
COPY --from=builder /home/app/build /usr/share/nginx/html
# Add nginx.config
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copy setup nginx entrypoint file
COPY ./scripts/entrypoint.sh .
# Expose ports
EXPOSE 3000
EXPOSE 80
# Execute script
RUN chmod +x ./entrypoint.sh
ENV NODE_ENV production
ENV USER_NAME=node_user USER_UID=2000 GROUP_NAME=node_group GROUP_UID=2000
# ENV USER_NAME=node_user USER_UID=2000 GROUP_NAME=node_group GROUP_UID=2000
RUN deluser --remove-home node \
&& addgroup --g ${GROUP_UID} -S ${GROUP_NAME} \
&& adduser -D -S -s /sbin/nologin -u ${USER_UID} -G ${GROUP_NAME} ${USER_NAME}
USER "${USER_NAME}"
# RUN deluser --remove-home node \
# && addgroup --g ${GROUP_UID} -S ${GROUP_NAME} \
# && adduser -D -S -s /sbin/nologin -u ${USER_UID} -G ${GROUP_NAME} ${USER_NAME}
# USER "${USER_NAME}"
ENTRYPOINT ["./entrypoint.sh"]
ENTRYPOINT [ "npm","start"]
# Start serving
CMD ["nginx", "-g", "daemon off;"]

View File

@ -1,6 +1,9 @@
#! /bin/bash
# no verbose
set +x
# save env to file
printenv > .env.production
# config
envFilename='.env.production'
resolvingPath='/usr/share/nginx/html'

View File

@ -1,44 +1,30 @@
import type { ArticleStore } from "../domain/articleStore";
import { useCallback, useEffect } from "react";
import { getArticle } from "article/data/articleAPIService";
import { Article } from "article/domain/articleEntity";
import { GetArticleUseCase } from "article/useCases/getArticleUseCase";
import { FetchArticleUseCase } from "article/useCases/fetchArticleUseCase";
import { useParams } from "react-router-dom";
function useArticleViewModel(
store: ArticleStore,
fetchArticleUseCase: (
fetchArticleCallback: (id: string) => Promise<Article | null>,
setArticle: ArticleStore["setArticle"],
id: string,
) => Promise<Article | null>,
getArticleUseCase: (
getArticle: ArticleStore["getArticle"],
setArticle: ArticleStore["setArticle"],
id: Article["id"],
) => Promise<Article | null>,
id: string,
fetchArticleUseCase: FetchArticleUseCase,
getArticleUseCase: GetArticleUseCase,
) {
let article: Article | null | undefined;
const { id } = useParams();
const _getArticle = useCallback(
(id: string) => {
getArticleUseCase(store.getArticle, store.setArticle, id).then(async (value) => {
if (value == null) {
article = await fetchArticleUseCase(getArticle, store.setArticle, id);
return;
}
article = value;
});
const getArticle = useCallback(
() => {
getArticleUseCase.call(id ?? '').catch((_) => fetchArticleUseCase.call(id ?? ''));
console.log(id);
},
[store.setArticle]
[id]
);
useEffect(() => {
_getArticle(id);
}, [article]);
useEffect(getArticle, []);
return {
article: store.article,
shouldShowLoading: typeof store.article === "undefined" || store.isLoading,
article: store.currentArticle,
shouldShowLoading: store.isLoading,
hasError: store.hasError,
};
}

View File

@ -7,7 +7,7 @@ import { integratorApiClient } from "core/httpClient";
const articleEndpoint = "/papers/"
async function getArticle(id: string): Promise<Article> {
async function fetchArticle(id: string): Promise<Article> {
try {
const response = await integratorApiClient.get<FetchArticleByIdDTO>(
// `https://run.mocky.io/v3/62cd4581-d864-4d46-b1d6-02b45b5d1994/${id}`
@ -33,4 +33,4 @@ async function getArticle(id: string): Promise<Article> {
}
}
export { getArticle };
export { fetchArticle };

View File

@ -1,22 +1,25 @@
import type { Article } from "../domain/articleEntity";
import { getArticle as getArticleAPI } from "./articleAPIService";
import * as actionTypes from "./articleActionTypes";
import { dispatchStatus } from "../../store/index";
const setArticleAction = (article: Article) => (dispatch: any) =>
dispatch({ type: actionTypes.SET_ARTICLE, article });
const setArticleAction = (article: Article, articlesList: Array<Article>) => (dispatch: any) => {
if (!articlesList.includes(article)) {
const updatedList = articlesList.concat([article]);
dispatch({ type: actionTypes.SET_ARTICLE, article, updatedList });
}
}
const getArticleAction = (id: string) => (dispatch: any) => {
const getArticleAction = (id: string, articles: Array<Article>) => (dispatch: any) => {
const filteredArray = articles.filter((e) => e.id == id);
if (filteredArray.length > 0) {
const article = filteredArray[0];
dispatchStatus(actionTypes.GET_ARTICLE, ".success", article)(dispatch);
return article;
}
return getArticleAPI(id)
.then((article) => {
dispatchStatus(actionTypes.GET_ARTICLE, ".success", article)(dispatch);
return article;
})
.catch((reason) => {
dispatchStatus(actionTypes.GET_ARTICLE, ".failure", reason)(dispatch);
return reason;
});
const reason = 'Article not in the store';
dispatchStatus(actionTypes.GET_ARTICLE, ".failure", reason)(dispatch);
return null;
};
export { setArticleAction, getArticleAction };

View File

@ -2,37 +2,58 @@ import React, { useCallback, useState } from "react";
import { useDispatch } from "react-redux";
import { ArticleStore } from "../domain/articleStore";
import type { Article } from "../domain/articleEntity";
import { getArticle as getArticleAPI } from "./articleAPIService";
const useArticleCommonStore = (): ArticleStore => {
const [isLoading, setLoading] = useState<boolean>(false);
const [hasError, setError] = useState<boolean>(false);
const [article, setArticleState] = useState<Article | undefined>();
const [currentArticle, setCurrentArticle] = useState<Article | null>(null);
const [articles, setArticlesState] = useState<Array<Article>>([]);
const dispatch = useDispatch();
const getArticle = useCallback(
async (id: string) => {
(id: string) => {
setLoading(true);
try {
const article = await getArticleAPI(id);
setArticleState(article);
if (typeof currentArticle === undefined) {
const fromStore = findArticleFromStore(id);
if (typeof fromStore === undefined) {
setError(true);
return null;
}
setLoading(false);
return article;
} catch (error) {
setError(true);
return null;
setCurrentArticle(fromStore);
return fromStore;
}
setLoading(false);
return currentArticle;
},
[dispatch]
);
const findArticleFromStore = (id: string): Article | null => {
const filteredArray = articles.filter((e) => e.id == id);
if (filteredArray.length > 0) {
return filteredArray[0];
}
return null;
}
const setNewArticle = (newArticle: Article) => {
setCurrentArticle(newArticle);
if (!articles.includes(newArticle)) {
setArticlesState(articles.concat([newArticle]));
} else if (articles.length == 0) {
setArticlesState([newArticle]);
}
}
return {
article: article,
articles: articles,
currentArticle: currentArticle,
isLoading,
hasError,
setArticle: setArticleState,
getArticle,
setArticle: setNewArticle,
getArticle: getArticle,
};
};

View File

@ -5,7 +5,8 @@ import * as actionTypes from "./articleActionTypes";
type ArticleStoreState = Omit<ArticleStore, "getArticle" | "setArticle">;
const INITIAL_STATE: ArticleStoreState = {
article: undefined,
articles: [],
currentArticle: null,
isLoading: false,
hasError: false,
};
@ -16,11 +17,11 @@ const articleReducer = (
): ArticleStoreState => {
switch (action.type) {
case actionTypes.SET_ARTICLE:
return { ...state, article: action.article };
return { ...state, articles: action.updatedList, currentArticle: action.article };
case actionTypes.GET_ARTICLE:
return { ...state, isLoading: true };
case actionTypes.GET_ARTICLE_SUCCESS:
return { ...state, isLoading: false, article: action.payload };
return { ...state, isLoading: false, currentArticle: action.payload };
case actionTypes.GET_ARTICLE_FAILURE:
return { ...state, hasError: true, isLoading: false };
default:

View File

@ -9,22 +9,23 @@ import { RootState, useAppSelector } from "store";
const articleSelector = (state: RootState): ArticleStoreState => state.article;
const useArticleStore = (): ArticleStore => {
const { isLoading, article, hasError } = useAppSelector(articleSelector);
const { isLoading, hasError, currentArticle, articles } = useAppSelector(articleSelector);
const dispatch = useDispatch();
const setArticle = useCallback(
(article: Article) => setArticleAction(article)(dispatch),
(article: Article) => setArticleAction(article, articles)(dispatch),
[dispatch]
);
const getArticle = useCallback(
(id: string) => getArticleAction(id)(dispatch),
(id: string) => getArticleAction(id, articles)(dispatch),
[dispatch]
);
return {
article: article,
articles: articles,
currentArticle: currentArticle,
isLoading,
hasError,
setArticle,

View File

@ -1,13 +1,14 @@
import { Article } from './articleEntity';
interface ArticleStore {
// State
article: Article | undefined;
articles: Array<Article>;
currentArticle: Article | null;
isLoading: boolean;
hasError: boolean;
// Actions
setArticle(article?: Article): void;
getArticle(identifier: string): Promise<Article | null>;
setArticle(article: Article): void;
getArticle(identifier: string): Article | null;
}
export type { ArticleStore };

View File

@ -1,6 +1,32 @@
import { Article } from "article/domain/articleEntity";
import { ArticleStore } from "article/domain/articleStore";
class FetchArticleUseCase {
/* ------------------------------ Dependencies ------------------------------ */
_fetchArticleCallback: (id: string) => Promise<Article | null>;
_store: ArticleStore;
/* -------------------------------------------------------------------------- */
constructor(
fetchArticle: (id: string) => Promise<Article | null>,
store: ArticleStore,
) {
this._fetchArticleCallback = fetchArticle;
this._store = store;
}
/* ----------------------------- Implementation ----------------------------- */
async call(id: string): Promise<Article | null> {
return this._fetchArticleCallback(id).then((article) => {
if (article != null) {
this._store.setArticle(article);
}
return article;
})
}
/* -------------------------------------------------------------------------- */
}
const fetchArticleUseCase = async (
fetchArticleCallback: (id: string) => Promise<Article | null>,
setArticle: ArticleStore["setArticle"],
@ -12,5 +38,5 @@ const fetchArticleUseCase = async (
}
return article;
};
export { FetchArticleUseCase };
export { fetchArticleUseCase };

View File

@ -1,6 +1,28 @@
import { Article } from "article/domain/articleEntity";
import type { ArticleStore } from "../domain/articleStore";
class GetArticleUseCase {
/* ------------------------------ Dependencies ------------------------------ */
_store: ArticleStore;
/* -------------------------------------------------------------------------- */
constructor(
store: ArticleStore,
) {
this._store = store;
}
/* ----------------------------- Implementation ----------------------------- */
async call(id: string): Promise<Article> {
const storedArticle = this._store.getArticle(id);
if (storedArticle != null) {
this._store.setArticle(storedArticle);
return storedArticle;
}
throw new Error('Article not found');
}
/* -------------------------------------------------------------------------- */
}
const getArticleUseCase = async (
getArticle: ArticleStore["getArticle"],
setArticle: ArticleStore["setArticle"],
@ -13,4 +35,5 @@ const getArticleUseCase = async (
return article;
};
export { GetArticleUseCase };
export { getArticleUseCase };

View File

@ -10,27 +10,20 @@ import { SVGSearch } from "components/icons";
import BaseLayout from "components/BaseLayout";
import Typography from "components/typography/Typography";
import { useTranslation } from "react-i18next";
import { fetchArticleUseCase } from "article/useCases/fetchArticleUseCase";
import { getArticleUseCase } from "article/useCases/getArticleUseCase";
import { FetchArticleUseCase } from "article/useCases/fetchArticleUseCase";
import { GetArticleUseCase } from "article/useCases/getArticleUseCase";
import { fetchArticle } from "article/data/articleAPIService";
const AnArticle = () => {
const store = useArticleStore();
const { id } = useParams();
const { article, hasError, shouldShowLoading } = useArticleViewModel(
store,
fetchArticleUseCase,
getArticleUseCase,
id ?? '',
new FetchArticleUseCase(fetchArticle, store),
new GetArticleUseCase(store),
);
const { i18n, t } = useTranslation();
// const { id } = useParams();
// const newId = `${id}`;
// useEffect(() => {
// store.getArticle(newId);
// }, [id]);
if (hasError) {
return <NotFound />;
}

View File

@ -13,21 +13,17 @@ import BaseLayout from "components/BaseLayout";
import Container from "components/Container";
import NotFound from "./NotFound";
import Markdown from "components/Markdown";
import { fetchArticleUseCase } from "article/useCases/fetchArticleUseCase";
import { getArticleUseCase } from "article/useCases/getArticleUseCase";
import { FetchArticleUseCase } from "article/useCases/fetchArticleUseCase";
import { GetArticleUseCase } from "article/useCases/getArticleUseCase";
import { fetchArticle } from "article/data/articleAPIService";
const AnArticleBody = () => {
const store = useArticleStore();
const { id } = useParams();
const { article, hasError, shouldShowLoading } = useArticleViewModel(
store,
fetchArticleUseCase,
getArticleUseCase,
id ?? '',
new FetchArticleUseCase(fetchArticle, store),
new GetArticleUseCase(store),
);
// useEffect(() => {
// store.getArticle(newId);
// }, [id]);
if (hasError) <NotFound />;
return (
<BaseLayout>