I'm using redux with redux-persist and redux-thunk.
Only some part of state is not being persisted. what could be the reason? is it a way to force persist the current state? When I call reducers, state gets updated. but when I reload the app, some part of state is still the old one. (empty)
I thought it is taking some parts of state from initialstate, so I added some entries to initialstate, even so, it's returning empty objects. not getting them from initialState.
Thanks in advance.
only discussionStudents gets persisted
Store setup:
import React from "react";
import { View, AsyncStorage } from 'react-native';
import { applyMiddleware, createStore, compose } from 'redux';
import { Provider } from 'react-redux';
import { persistStore, autoRehydrate } from 'redux-persist';
import thunk from 'redux-thunk';
import { createLogger } from 'redux-logger';
import { reducer } from './reducers';
import Container from './Container';
const middlewares = [thunk];
const logger = createLogger();
middlewares.push(logger);
const store = createStore(reducer, compose(
applyMiddleware(...middlewares)
), autoRehydrate({ log: true }));
persistStore(store, {storage: AsyncStorage});
const Root = () => (
<Provider store={store}>
<Container />
</Provider>
);
export default Root;
parts of the reducer :
import {REHYDRATE} from 'redux-persist/constants';
export const types = {
SYNCHRONISE_DISCUSSIONS: 'SYNCHRONISE_DISCUSSIONS'
};
export const actionCreators = {
synchroniseDiscussions: (args) => {
return dispatch => {
/// Call API
synchroniseDiscussionsAPI()
.then((res) => {
return dispatch(synchroniseDiscussions(res))
})
.catch((e) => {
console.log(e)
})
}
}
}
const synchroniseDiscussions = (args) => {
return {type: types.SYNCHRONISE_DISCUSSIONS, payload: args}
}
const initialState = {
rehydrated: false,
discussionStudents: [],
discussionGroups: [],
discussionsMessages: [],
discussionParticipants: []
}
export const reducer = (state = initialState, action) => {
const {
discussionStudents,
discussionGroups,
discussionsMessages,
discussionParticipants
} = state;
const {type, payload} = action;
switch (type) {
case types.SYNCHRONISE_DISCUSSIONS:
{
const oldStudents = discussionStudents
const newStudents = payload.discussionStudents
var parsedStudents = []
oldStudents.forEach((old, i)=>{
if(newStudents.findIndex(newstd => newstd.userId == old.userId) < 0){
parsedStudents.push({
...old,
status: 'inactive'
})
}
})
newStudents.forEach((newStudent, i)=>{
if(parsedStudents.findIndex(pstd => pstd.userId == newStudent.userId) < 0){
parsedStudents.push({
...newStudent,
status: 'active'
})
}
})
var newdiscussionParticipants = payload.discussionParticipants
var newdiscussionGroups = payload.discussionGroups
return Object.assign({}, state, {
discussionStudents: parsedStudents,
discussionParticipants: newdiscussionParticipants,
discussionGroups: newdiscussionGroups
})
}
case REHYDRATE:
{
return {
...state,
rehydrated: true
}
}
}
return state
}
I've found the issue. Issue was that I was using for loops inside the function. and promise was being resolved before the loop finishes. Issue solved by replacing the built in javascript loop with a custom asynchronous loop :
const asyncLoop = (iterations, func, callback) => {
var index = 0;
var done = false;
var loop = {
next: function() {
if (done) {
return;
}
if (index < iterations) {
index++;
func(loop);
} else {
done = true;
callback();
}
},
iteration: function() {
return index - 1;
},
break: function() {
done = true;
callback();
}
};
loop.next();
return loop;
}
Related
I am a newbie in React and Next JS, I want to set initial auth user data on initial load from the __app.js. But using dispatch throwing error "Invalid hook call". I know according to docs calling hooks in render function is wrong. but I am looking for an alternate solution to this.
How I can set auth data one-time so that will be available for all the pages and components.
I am including my code below.
/contexts/app.js
import { useReducer, useContext, createContext } from 'react'
const AppStateContext = createContext()
const AppDispatchContext = createContext()
const reducer = (state, action) => {
switch (action.type) {
case 'SET_AUTH': {
return state = action.payload
}
default: {
throw new Error(`Unknown action: ${action.type}`)
}
}
}
export const AppProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, {})
return (
<AppDispatchContext.Provider value={dispatch}>
<AppStateContext.Provider value={state}>
{children}
</AppStateContext.Provider>
</AppDispatchContext.Provider>
)
}
export const useAuth = () => useContext(AppStateContext)
export const useDispatchAuth = () => useContext(AppDispatchContext)
/_app.js
import 'bootstrap/dist/css/bootstrap.min.css'
import '../styles/globals.css'
import App from 'next/app'
import Layout from '../components/Layout'
import { mutate } from 'swr'
import { getUser } from '../requests/userApi'
import { AppProvider, useDispatchAuth } from '../contexts/app'
class MyApp extends App {
render() {
const dispatchAuth = useDispatchAuth()
const { Component, pageProps, props } = this.props
// Set initial user data
const setInitialUserData = async () => {
if (props.isServer) {
const initialData = {
loading: false,
loggedIn: (props.user) ? true : false,
user: props.user
}
const auth = await mutate('api-user', initialData, false)
dispatchAuth({
type: 'SET_AUTH',
payload: auth
})
}
}
//----------------------
// Set initial user data
setInitialUserData()
//----------------------
return (
<AppProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</AppProvider>
)
}
}
MyApp.getInitialProps = async (appContext) => {
let isServer = (appContext.ctx.req) ? true : false
let user = null
let userTypes = {}
// Get user server side
if (isServer) {
await getUser()
.then(response => {
let data = response.data
if (data.status == true) {
// Set user
user = data.data.user
userTypes = data.data.user_types
//---------
}
})
.catch(error => {
//
})
}
//---------------------
return {
props: {
user,
userTypes,
isServer
}
}
}
export default MyApp
I believe this is the intended use of the useEffect hook with an empty array as its second argument:
https://reactjs.org/docs/hooks-effect.html
import {useEffect} from 'react'
class MyApp extends App {
useEffect(()=> {
setInitialUserData()
},[])
render() {
...
}
}
The Redux Update operations I made on the client-side in Next JS are not updated in the server store.
Hello. I have a problem. I'm developing an SSR application with Next JS. I have provided the next js link with the next-redux-wrapper. State update operations can be provided. State updates I made on the server-side can be viewed on the client-side. The redux updates I made on the client-side also appear on the client-side, but when I refresh the page, it returns to the previous position. Sample scenario:
Users have addresses. Their addresses can be taken from the DB and printed on the screen. DB updates when I add a new address or delete the old address. Along with it, it is updated in the store on the client-side. So far there is no problem. However, when I refresh the page, for example, if there are 4 addresses before updating and I deleted one, after the refresh, it is printed as 4 addresses again. It continues like this until I get data from the server again.
How can I move the client-side store updates to the server-side without having to make requests to the server over and over again?
store.js
// store.js
import { createStore, applyMiddleware } from 'redux';
import { createWrapper } from "next-redux-wrapper";
import thunkMiddleware from 'redux-thunk'
// ROOT REDUCERS
import rootReducer from "../reducers";
const bindMiddleware = (middleware) => {
if (process.env.NODE_ENV !== 'production') {
const { composeWithDevTools } = require('redux-devtools-extension')
return composeWithDevTools(applyMiddleware(...middleware))
}
return applyMiddleware(...middleware)
}
const store_ = (initialState) => {
return createStore(rootReducer, initialState, bindMiddleware([thunkMiddleware]));
}
const wrapper = createWrapper(store_/*, { debug: true }*/);
export {
wrapper
}
_app.js
// _app.js
const MyApp = ({props, Component, pageProps }) => {
const store = useStore();
if (!store.getState().R_PageSettings.initStore)
{
store.dispatch({
type: HYDRATE,
payload: {
...props.initialState
}
})
}
return (
<>
<Head>
<title>{ variables.meta.title }</title>
</Head>
<Component {...pageProps} />
</>
)
}
const wrappedApp = wrapper.withRedux(MyApp);
export default wrappedApp;
wrappedApp.getInitialProps = async ctx => {
const data = await wrapper.getServerSideProps(
async (req) => {
const { store, ctx } = req;
const reduxStates = store.getState();
let user = reduxStates.R_User.user;
if (!user)
{
const cookies = parseCookies(ctx);
if (cookies.usr && user !== undefined)
{
const getUser = await CustomersController.tokenLoginControl(cookies.usr);
if (getUser && getUser.status)
{
store.dispatch(setUserSSR(getUser.user))
user = getUser.user;
}
else
destroyCookie(ctx, 'usr');
}
}
return {
user
}
}
)(ctx)
return data;
}
action.js
// CONSTANTS
import {
C_User
} from "../constants";
export const setUserSSR = user => {
return {
type: C_User.SET_USER,
payload: {
user
}
}
}
export const setUser = user => dispatch => {
return dispatch({
type: C_User.SET_USER,
payload: {
user
}
})
}
addresspage.js
// addresspage.js
import { connect } from 'react-redux';
import { bindActionCreators } from "redux";
// COMPONENTS
import UserPageLayout from "../UserPagesLayout";
import {
CustomerAddressForm
} from "../../../components";
// CONTROLLERS
import {
CustomersController
} from "../../../controllers";
// ACTIONS
import {
setUser
} from "../../../actions";
const MyAddressPage = connect(({ R_User }) => {
return {
R_User
}
}, dispatch => {
return {
setUser: bindActionCreators(setUser, dispatch)
}
})((props) => {
const addAddressHandle = () => {
props.fullBarOpen(
<CustomerAddressForm confirmHandle={async (address, setLoading) => {
const execute = await CustomersController.addAddress(address);
if (execute.status)
{
await props.setUser(execute.user);
}
else
{
setLoading(false);
}
}}
/>
);
}
return (
<UserPageLayout>
</UserPageLayout>
);
})
export default MyAddressPage;
I am working on a React application and I am using Redux to store the state. I have the following code:
menu.reducer.js:
import { INCREASE_CATEGORY_RANK, DECREASE_CATEGORY_RANK } from './menu.types';
const INITIAL_STATE = []
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case INCREASE_CATEGORY_RANK: {
console.log("Printing state");
console.log(state);
const menuArray = [...state.menu];
menuArray.sort((a,b) => (a.rank > b.rank) ? 1 : -1);
return {
...state,
menu: menuArray
}
}
case DECREASE_CATEGORY_RANK: {
return state.map(category => {
if (category._id === action.payload._id && category.rank !== -1) {
const oldrank = category.rank;
return {
...category,
rank: oldrank - 1
}
} else {
return category;
}
})
}
default:
return state;
}
}
menu.types.js:
export const INCREASE_CATEGORY_RANK = "INCREASE_CATEGORY_RANK";
export const DECREASE_CATEGORY_RANK = "DECREASE_CATEGORY_RANK";
menu.actions.js:
import { apiUrl, apiConfig } from '../../util/api';
import { INCREASE_CATEGORY_RANK, DECREASE_CATEGORY_RANK } from './menu.types';
export const getMenu = () => async dispatch => {
const response = await fetch(`${apiUrl}/menu`);
if (response.ok) {
const menuData = await response.json();
dispatch({ type: GET_MENU, payload: menuData })
}
}
export const increaseCategoryRank = category => dispatch => {
dispatch({ type: INCREASE_CATEGORY_RANK, payload: category })
}
export const decreaseCategoryRank = category => dispatch => {
dispatch({ type: DECREASE_CATEGORY_RANK, payload: category })
}
category-arrows.component.jsx:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { increaseCategoryRank, decreaseCategoryRank } from '../../redux/menu/menu.actions';
import './category-arrows.styles.scss';
class CategoryArrows extends Component {
render() {
const { category, increaseCategoryRank, decreaseCategoryRank } = this.props;
return (
<div class="arrows-container">
<div class="up-arrow" onClick={() => this.props.increaseCategoryRank(category)}></div>
<div class="category-rank">
<p>{category.rank}</p>
</div>
<div class="down-arrow" onClick={() => this.props.decreaseCategoryRank(category)}></div>
</div>
)
}
}
export default connect(null, { increaseCategoryRank, decreaseCategoryRank } )(CategoryArrows);
For my Reducer function, the initial state is retrieved from a database. The Reducer code deals with the menu array, which is an array of objects:
I want to copy the menu array from the state in my Reducer function, so that I can sort it, and then reassign the sorted menu array to the state.
I have tried to console.log() the state to see what it is in my Reducer function. When I click on the up-arrow div in category-arrows.component.jsx, the INCREASE_CATEGORY_RANK action is dispatched. When I check the Console, I get the following:
However when I copy the menu array from the state in the INCREASE_CATEGORY_RANK case in my Reducer function, I get the following error:
I am not sure why I am getting the above error and how to resolve it. Any insights are appreciated.
It looks like the reducer expect an array not an object:
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case INCREASE_CATEGORY_RANK: {
return [...state].sort((a,b) => (a.rank > b.rank) ? 1 : -1);
}
}
}
I had asked this question before.
I looked carefully at the exchange section, which I advised, and I think there is no problem with the exchange section.
At least in my opinion there is no problem and I hardly know the cause of the problem.
And I was so frustrated that I put everything in the code.
If anyone can provide us with a clue to this problem, please reply to me.
interactions.js
import Web3 from 'web3'
import {
web3Loaded,
web3AccountLoaded,
tokenLoaded,
exchangeLoaded,
cancelledOrdersLoaded
} from './actions'
import Token from '../abis/Token.json'
import Exchange from '../abis/Exchange.json'
export const loadWeb3 = (dispatch) => {
const web3 = new Web3(Web3.givenProvider || 'http://localhost:7545')
dispatch(web3Loaded(web3))
return web3
}
export const loadAccount = async (web3, dispatch) => {
const accounts = await web3.eth.getAccounts()
const account = accounts[0]
dispatch(web3AccountLoaded(account))
return account
}
export const loadToken = async (web3, networkId, dispatch) => {
try {
const token = new web3.eth.Contract(Token.abi, Token.networks[networkId].address)
dispatch(tokenLoaded(token))
return token
} catch (error) {
console.log('Contract not deployed to the current network. Please select another network with Metamask.')
return null
}
}
export const loadExchange = async (web3, networkId, dispatch) => {
try {
const exchange = new web3.eth.Contract(Exchange.abi, Exchange.networks[networkId].address)
dispatch(exchangeLoaded(exchange))
return exchange
} catch (error) {
console.log('Contract not deployed to the current network. Please select another network with Metamask.')
return null
}
}
export const loadAllOrders = async (exchange, dispatch) => {
// if (exchange) { // Make sure exchange has been defined
// const exchange = new web3.eth.Contract(Exchange.abi, Exchange.networks[networkId].address)
const cancelStream = await exchange.getPastEvents('Cancel', { fromBlock: 0, toBlock: 'latest' })
// // await loadAllOrders(this.props.exchange, dispatch)
console.log(cancelStream)
}
App.js
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import Navbar from './Navbar'
import Web3 from 'web3';
import Content from './Content'
import { connect } from 'react-redux'
// import Token from '../abis/Token.json'
import {
loadWeb3,
loadAccount,
loadToken,
loadExchange
} from '../store/interactions'
import { contractsLoadedSelector } from '../store/selectors'
class App extends Component {
componentWillMount() {
this.loadBlockchainData(this.props.dispatch)
}
async loadBlockchainData(dispatch) {
const web3 = loadWeb3(dispatch)
const network = await web3.eth.net.getNetworkType()
const networkId = await web3.eth.net.getId()
const accounts = await loadAccount(web3, dispatch)
const token = await loadToken(web3, networkId, dispatch)
if(!token) {
window.alert('Token smart contract not detected on the current network. Please select another network with Metamask.')
return
}
const exchange = await loadExchange(web3, networkId, dispatch)
if(!exchange) {
window.alert('Exchange smart contract not detected on the current network. Please select another network with Metamask.')
return
}
}
render() {
return (
<div>
<Navbar />
{ this.props.contractsLoaded ? <Content /> : <div className="content"></div> }
</div>
);
}
}
function mapStateToProps(state) {
return {
contractsLoaded: contractsLoadedSelector(state)
}
}
export default connect(mapStateToProps)(App);
reducers.js
import { combineReducers } from 'redux';
function web3(state={}, action) {
switch (action.type) {
case 'WEB3_LOADED':
return { ...state, connection: action.connection }
case 'WEB3_ACCOUNT_LOADED':
return { ...state, account: action.account }
default:
return state
}
}
function token(state = {}, action) {
switch (action.type) {
case 'TOKEN_LOADED':
return { ...state, loaded: true, contract: action.contract }
default:
return state
}
}
function exchange(state = {}, action) {
switch (action.type) {
case 'EXCHANGE_LOADED':
return { ...state, loaded: true, contract: action.contract }
case 'CANCELLED_ORDERS_LOADED':
return { ...state, cancelledOrders: { loaded: true, data: action.cancelledOrders } }
// case 'FILLED_ORDERS_LOADED':
// return { ...state, filledOrders: { loaded: true, data: action.filledOrders } }
// case 'ALL_ORDERS_LOADED':
// return { ...state, allOrders: { loaded: true, data: action.allOrders } }
default:
return state
}
}
const rootReducer = combineReducers({
web3,
token,
exchange
})
export default rootReducer
Content.js
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { exchangeSelector } from '../store/selectors'
import { loadAllOrders } from '../store/interactions'
class Content extends Component {
componentWillMount() {
this.loadBlockchainData(this.props.dispatch)
}
// async loadBlockchainData(exchange, dispatch) {
async loadBlockchainData(dispatch) {
await loadAllOrders(this.props.exchange, dispatch)
// this.loadBlockchainData(this.props.exchange)
// await loadAllOrders(exchange, dispatch)
}
function mapStateToProps(state) {
return {
exchange: state.exchangeSelector
}
}
export default connect(mapStateToProps)(Content)
selectors.js
import { get } from 'lodash'
import { createSelector } from 'reselect'
const account = state => get(state, 'web3.account')
export const accountSelector = createSelector(account, a => a)
const tokenLoaded = state => get(state, 'token.loaded', false)
export const tokenLoadedSelector = createSelector(tokenLoaded, tl => tl)
const exchangeLoaded = state => get(state, 'exchange.loaded', false)
export const exchangeLoadedSelector = createSelector(exchangeLoaded, el => el)
const exchange = state => get(state, 'exchange.contract')
export const exchangeSelector = createSelector(exchange, e => e)
export const contractsLoadedSelector = createSelector(
tokenLoaded,
exchangeLoaded,
(tl, el) => (tl && el)
)
Check exchange to make sure not undefined
export const loadAllOrders = async (exchange, dispatch) => {
const cancelStream = exchange ?
await exchange.getPastEvents('Cancel', { fromBlock: 0, toBlock: 'latest' })
: null // Check if exchange defined then call getPastEvents
console.log(cancelStream)
}
In my ReactJS / Typescript app, I'm getting the following error in my store.ts:
Parameter 'initialState' cannot be referenced in its initializer.
interface IinitialState {
fiatPrices: [];
wallets: [];
defaultCurrency: string;
}
const initialState = {
fiatPrices: [],
wallets: [],
defaultCurrency: ''
}
...
export function initializeStore (initialState:IinitialState = initialState) {
return createStore(
reducer,
initialState,
composeWithDevTools(applyMiddleware(thunkMiddleware))
)
}
Anyone else run into this issue? Currently having to rely on // #ts-ignore
Entire store.ts file:
import { createStore, applyMiddleware } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunkMiddleware from 'redux-thunk'
interface IinitialState {
fiatPrices: [];
wallets: [];
defaultCurrency: string;
}
const initialState = {
fiatPrices: [],
wallets: [],
defaultCurrency: ''
}
export const actionTypes = {
GET_PRICES: 'GET_PRICES'
}
// REDUCERS
export const reducer = (state = initialState, action: any) => {
switch (action.type) {
case actionTypes.GET_PRICES:
return state
default:
return state
}
}
// MOCK API
export async function getProgress(dispatch: any) {
try {
const priceList = await fetchPrices();
return dispatch({ type: actionTypes.GET_PRICES, payload: priceList })
}
catch (err) {
console.log('Error', err);
}
}
// Wait 1 sec before resolving promise
function fetchPrices() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ progress: 100 });
}, 1000);
});
}
// ACTIONS
export const addLoader = () => (dispatch: any) => {
getProgress(dispatch);
}
// #ts-ignore
export function initializeStore (initialState:IinitialState = initialState) {
return createStore(
reducer,
initialState,
composeWithDevTools(applyMiddleware(thunkMiddleware))
)
}
withReduxStore lib file
import React from 'react'
import { initializeStore, IinitialState } from '../store'
const isServer = typeof window === 'undefined'
const __NEXT_REDUX_STORE__ = '__NEXT_REDUX_STORE__'
function getOrCreateStore (initialState: IinitialState) {
// Always make a new store if server, otherwise state is shared between requests
if (isServer) {
return initializeStore(initialState)
}
// Create store if unavailable on the client and set it on the window object
// Waiting for (#ts-ignore-file) https://github.com/Microsoft/TypeScript/issues/19573 to be implemented
// #ts-ignore
if (!window[__NEXT_REDUX_STORE__]) {
// #ts-ignore
window[__NEXT_REDUX_STORE__] = initializeStore(initialState)
}
// #ts-ignore
return window[__NEXT_REDUX_STORE__]
}
// #ts-ignore
export default App => {
return class AppWithRedux extends React.Component {
// #ts-ignore
static async getInitialProps (appContext) {
// Get or Create the store with `undefined` as initialState
// This allows you to set a custom default initialState
const reduxStore = getOrCreateStore()
// Provide the store to getInitialProps of pages
appContext.ctx.reduxStore = reduxStore
let appProps = {}
if (typeof App.getInitialProps === 'function') {
appProps = await App.getInitialProps(appContext)
}
return {
...appProps,
initialReduxState: reduxStore.getState()
}
}
// #ts-ignore
constructor (props) {
super(props)
this.reduxStore = getOrCreateStore(props.initialReduxState)
}
render () {
return <App {...this.props} reduxStore={this.reduxStore} />
}
}
}
function initializeStore (initialState:IinitialState = initialState) { ... }
is not valid by any means, not just in TypeScript. It's incorrect to suppress the error with #ts-ignore.
initialState parameter shadows the variable of the same name from enclosing scope, so default parameter value refers the parameter itself. This will result in discarding default parameter value with ES5 target and in an error with ES6 target.
The parameter and default value should have different names:
function initializeStore (initialState:IinitialState = defaultInitialState) { ... }
Notice that the use of defaultInitialState isn't needed in a reducer, due to how initial state works. Initial state from createStore takes precedence if combineReducers is not in use.