<header>reducers</header> <body> <div id="testDiv"> Test </div> <script> const delay = ms => new Promise(ok => setTimeout(() => ok(ms), ms)) // для наглядности отладки function jwtDecode(token) { // расщифровки токена авторизации if (!token || typeof token != "string") return undefined; let tokenArr = token.split("."); if (tokenArr.length != 3) return undefined; try { let tokenJsonStr = atob(tokenArr[1]); let tokenJson = JSON.parse(tokenJsonStr); return tokenJson; } catch { return undefined; } } function gql(url, query, vars) { // формирование запроса GQl let fetchSettings = { method: "POST", headers: { "Content-Type": "application/json", "Accept": "application/json" }, body: JSON.stringify( { query: query, variables: vars }) }; return fetch(url, fetchSettings).then(res => res.json()); } function signIn(login, password, url) { // авторизация через GQl const loginQuery = `query login($login:String, $password:String){ login(login:$login, password:$password) }`; return gql( url, loginQuery, { login: login, password: password }); } function treatAuth(action, state) { // обработка успешной авторизации и смена состояния let payload = action.payload; return setAuthState({ token: payload.data.login, payload: jwtDecode(payload.data.login) }) } function setAuthState(authPayload, state) { //смена состояния авторизации let newState = { ...state }; newState.auth = { ...state.auth }; newState.auth.payload = authPayload; return newState; } function treatBuy(action, state) { // обработка покупки } function promiseReducer(state = {}, action) { // диспетчер обработки if (action) { if (action.type === 'PROMISE') { let newState = treatPayload(action, "buy", state, treatBuy); newState = treatPayload(action, "auth", newState, treatAuth); return newState; } } return state; } function treatPayload(action, name, state, onPayloadFunc) { //вызов функций обработки состояния по статусу с сбросом текущего состояния и обработки payload let resultState = state; let actionName = action.name; if (actionName == name) { resultState = changeStatus(name, state, action, resetStateFunc); if (state !== resultState && action.status == "FULLFILLED") resultState = onPayloadFunc(action, resultState) } return resultState; } function changeStatus(name, state, action) { //обработка состояния по статусу let result = { ...state }; result[name] = { status: status, payload: undefined, error: actionData.error }; return result; } function createStore(reducer) { let state = reducer(undefined, {}) //стартовая инициализация состояния, запуск редьюсера со state === undefined let cbs = [] //массив подписчиков const getState = () => state //функция, возвращающая переменную из замыкания const subscribe = cb => (cbs.push(cb), //запоминаем подписчиков в массиве () => cbs = cbs.filter(c => c !== cb)) //возвращаем функцию unsubscribe, которая удаляет подписчика из списка function dispatch(action) { if (typeof action === 'function') { //если action - не объект, а функция return action(dispatch, getState) //запускаем эту функцию и даем ей dispatch и getState для работы } const newState = reducer(state, action) //пробуем запустить редьюсер if (newState !== state) { //проверяем, смог ли редьюсер обработать action state = newState //если смог, то обновляем state for (let cb of cbs) cb() //и запускаем подписчиков } } return { getState, //добавление функции getState в результирующий объект dispatch, subscribe //добавление subscribe в объект } } function actionPromise({ name, promise }) { return async function Exec(dispatch) { dispatch(actionPending(name)) //сигнализируем redux, что промис начался try { const payload = await promise //ожидаем промиса; dispatch(actionFulfilled(name, payload)); //сигнализируем redux, что промис успешно выполнен return payload //в месте запуска store.dispatch с этим thunk можно так же получить результат промиса } catch (error) { dispatch(actionRejected(name, error)) //в случае ошибки - сигнализируем redux, что промис несложился } }; } const actionPending = (name) => ({ type: 'PROMISE', name: name, status: 'PENDING' }); const actionFulfilled = (name, payload) => ({ type: 'PROMISE', name: name, payload: payload, status: 'FULFILLED' }); const actionRejected = (name, error) => ({ type: 'PROMISE', name: name, error: error, status: 'REJECTED' }); const store = createStore(promiseReducer) store.subscribe(() => { let state = store.getState(); if (state.buy) { if (state.buy.status == "PENDING") testDiv.style.color = "orange"; else if (state.buy.status == "FULLFILLED") testDiv.style.color = "green"; else if (state.buy.status == "REJECTED") { testDiv.style.color = "red"; testDiv.innerText = state.buy.errorMessage; } } }); let execFunc = actionPromise({ name: "auth", promise: signIn("test457", "123123", "http://shop-roles.node.ed.asmer.org.ua/graphql") }); store.dispatch(execFunc); /* const actionPending = () => ({ type: 'PROMISE', status: 'PENDING' }) const actionFulfilled = payload => ({ type: 'PROMISE', status: 'FULFILLED', payload }) const actionRejected = error => ({ type: 'PROMISE', status: 'REJECTED', error }) store.subscribe(() => console.log(store.getState())) store.dispatch({ type: 'COUNTER_INC' }) store.dispatch({ type: 'BOOLEAN_SET' }) store.dispatch({ type: 'COUNTER_INC' }) store.dispatch({ type: 'BOOLEAN_TOGGLE' }) store.dispatch({ type: 'COUNTER_DEC' }) store.dispatch({ type: 'ДИЧЬ' }) //не вызывает подписчика */ </script> </body> function combineReducers(reducers) { function totalReducer(totalState = {}, action) { const newTotalState = {} //объект, который будет хранить только новые состояния дочерних редьюсеров //цикл + квадратные скобочки позволяют написать код, который будет работать с любыми количеством дочерных редьюсеров for (const [reducerName, childReducer] of Object.entries(reducers)) { const newState = childReducer(totalState[reducerName], action) //запуск дочернего редьюсера if (newState !== totalState[reducerName]) { //если он отреагировал на action newTotalState[reducerName] = newState //добавляем его в newTotalState } } //Универсальная проверка на то, что хотя бы один дочерний редьюсер создал новый стейт: if (Object.values(newTotalState).length) { return { ...totalState, ...newTotalState } //создаем новый общий стейт, накладывая новый стейты дочерних редьюсеров на старые } return totalState //если экшен не был понят ни одним из дочерних редьюсеров, возвращаем общий стейт как был. } return totalReducer } ////// /////