What will happen when i use Redux Toolkit Query with redux-persist?
Will it use the persisted state or will the state be refetched?
There is an official guide for this now. At the time of writing this, you could configure it this way:
// store.ts
import { configureStore, combineReducers } from '#reduxjs/toolkit'
import {
persistStore,
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
} from 'redux-persist'
import AsyncStorage from '#react-native-async-storage/async-storage'
import someSlice from 'config/features/someSlice'
import { api } from 'config/services/api'
const reducers = combineReducers({
someSlice,
[api.reducerPath]: api.reducer,
})
const persistConfig = {
key: 'root',
version: 1,
storage: AsyncStorage,
}
const persistedReducer = persistReducer(persistConfig, reducers)
export const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
// Redux persist
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}).concat(
api.middleware,
),
})
export let persistor = persistStore(store)
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
// App.tsx for native or index.tsx
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>,
I just really wouldn't do it.
That restored data could be all kinds of stale and when a user hits F5 they usually expect data to be up-to-date, not a week old or something. Also, while the store slice is restored, information about subscriptions might be problematic (because the "subscribing components" never exist, they can also never unmount and thus get never cleaned from the store).
So, I'd blacklist the api slice from being persisted.
If you want that stuff to be cached, do it with cache headers in your server. The browser will do all the caching for you, but also allow the user to clear the cache or force a refetch with ctrl+shift+r - so the browser would just behave more than the user expects.
Use extractRehydrationInfo like this:
import { REHYDRATE } from 'redux-persist'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
extractRehydrationInfo(action, { reducerPath }) {
if (action.type === REHYDRATE) {
return action.payload[reducerPath]
}
...
},
Full official guide here.
Related
I have a React app where I've used the #rtk-query/codegen-openapi tool to generate my authentication queries and mutations. I'm also using Redux Persist to store some of this data.
I'm trying to figure out a way to invalidate this data programmatically when the user logs out. Is it possible to do this?
My redux store configuration looks something like this:
const persistConfig = {
key: "root",
storage,
blacklist: ["api"],
};
const authPersistConfig = {
key: "auth",
storage,
whitelist: [],
};
const rootReducer = combineReducers({
settings: settingsSlice.reducer,
[applicationApi.reducerPath]: applicationApi.reducer,
[enhancedAuthenticateApi.reducerPath]: persistReducer(
authPersistConfig,
enhancedAuthenticateApi.reducer
),
});
const persistedReducer = persistReducer(persistConfig, rootReducer);
export const buildStore = () =>
configureStore({
reducer: persistedReducer,
middleware: getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}).concat([enhancedAuthenticateApi.middleware]),
});
I'm pretty new to Redux and RTK Queries so any help is appreciated.
Problem
I have just recently found out about this library and was implementing it for a react native app, but can't get my head around how the effect reconciler passes the errors to rollback. I am firing the below action, and whether the response is success or failure, the effect reconciler directly passes everything to commit. can anyone tell me what I am doing wrong here?
const saveProfileRequest = (query, link): Action => ({
type: SAVE_PROFILE_PICTURE_REQUEST,
meta: {
offline: {
effect: {
url: BASE_URL,
method: 'POST',
body: JSON.stringify({
query,
variables: { url: link }
})
},
commit: { type: SAVE_PROFILE_PICTURE_SUCCESS, meta: { link } },
rollback: { type: SAVE_PROFILE_PICTURE_FAILURE }
}
}
});
Expectations
As I am currently implementing the offline capabilities to an existing app, I was expecting the effect reconciler to pass success response to commit and pass error response to rollback.
Store config
// #flow
import { createStore, applyMiddleware, combineReducers } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import { offline } from '#redux-offline/redux-offline';
import offlineConfig from '#redux-offline/redux-offline/lib/defaults';
import { persistStore, persistReducer } from 'redux-persist';
import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2';
import thunkMiddleware from 'redux-thunk';
import AsyncStorage from '#react-native-community/async-storage';
import type { Store } from '#types/Store';
import rootReducer from '#reducers/index';
const persistConfig = {
key: 'root',
storage: AsyncStorage,
whitelist: ['reducerOne', 'reducerTwo', 'reducerThree'],
stateReconciler: autoMergeLevel2
};
const persistedReducer = persistReducer(
persistConfig,
combineReducers(rootReducer)
);
const store: Store = createStore(
persistedReducer,
composeWithDevTools(
applyMiddleware(thunkMiddleware),
offline({
...offlineConfig,
retry(_action, retries) {
return (retries + 1) * 1000;
},
returnPromises: true
})
)
);
I was having the same issue, because our GraphQL API always return 200, but with an error object, so I needed to customize it.
Basically you need to customize the discard function, returning false in case of an error.
The default discard already return false in case of Network issue, but if you have errors inside your response, and it's returning 200 instead, you can customize.
Documentation for Discard: https://github.com/redux-offline/redux-offline/blob/develop/docs/api/config.md#discard
Default implementation (you can use that as initial draft): https://github.com/redux-offline/redux-offline/blob/develop/src/defaults/discard.js
I am getting this error even though I am using redux thunk. Redux store is also set up correctly (I think). I am creating a MERN app and I want to send a POST request to the backend (form submit) to create a new user. The request and response from the server are fine (checked using Postman). I just can not find where the problem is. The action creator which is causing this is :-
import axios from "axios";
export function signupUser(newuser) {
return function (dispatch) {
axios.post("/auth", newuser).then((res) => {
// dispatch
dispatch({
type: "ADDNEW_USER",
payload: res.data,
});
});
};
}
The store setup is :-
import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import rootReducer from "./reducers";
const initialState = {};
// const middleware = [thunk];
const store = createStore(
rootReducer,
initialState,
compose(
applyMiddleware(thunk),
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__()
)
);
export default store;
The Signup.js component is ->
[Signup.js][1]
and the error is -> [here][2]
[1]: https://paste.ubuntu.com/p/gCX3wQPt9X/
[2]: https://imgur.com/a/6SDmydw
I followed the example in the documentation under v2.0.0 > Read Me > Load Data (listeners automatically managed on mount/unmount) (direct link is not possible).
And replaced the connect call with the firestore specific one shown here](http://react-redux-firebase.com/docs/firestore.html#examples) in Example 1.
I copied the Todo example exactly in a new component created for testing purposes.
Todo Component:
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { compose } from 'redux'
import { firebaseConnect,firestoreConnect, isLoaded, isEmpty } from 'react-redux-firebase'
const Todos = ({ todos, firebase }) => {
// Build Todos list if todos exist and are loaded
const todosList = !isLoaded(todos)
? 'Loading'
: isEmpty(todos)
? 'Todo list is empty'
: Object.keys(todos).map(
(key, id) => (
<TodoItem key={key} id={id} todo={todos[key]}/>
)
)
return (
<div>
<h1>Todos</h1>
<ul>
{todosList}
</ul>
<input type="text" ref="newTodo" />
<button onClick={this.handleAdd}>
Add
</button>
</div>
)
}
// export default compose(
// firestoreConnect([
// 'todos' // { path: '/todos' } // object notation
// ]),
// connect((state) => ({
// todos: state.firestore.data.todos,
// profile: state.firestore.profile // load profile
// }))
// )(Todos)
export default compose(
firestoreConnect(['todos']), // or { collection: 'todos' }
connect((state, props) => ({
todos: state.firestore.ordered.todos
}))
)(Todos)
The store configuration was configured as shown here in the docs. The store configuration was adapted to slot into the framework created by react-boilerplate.
/**
* Create the store with dynamic reducers
*/
import { createStore, applyMiddleware, compose } from 'redux'
import { fromJS } from 'immutable'
import { routerMiddleware } from 'connected-react-router/immutable'
import createSagaMiddleware from 'redux-saga'
import { reactReduxFirebase, firebaseReducer } from 'react-redux-firebase'
import { reduxFirestore, firestoreReducer } from 'redux-firestore'
import firebase from 'firebase/app'
import 'firebase/auth'
import 'firebase/database'
import 'firebase/firestore'
import createReducer from './reducers'
const sagaMiddleware = createSagaMiddleware()
const firebaseConfig = {
apiKey: process.env.FIREBASE_API_KEY,
authDomain: process.env.AUTH_DOMAIN,
databaseURL: process.env.DATABASE_URL,
projectId: process.env.PROJECT_ID,
storageBucket: process.env.STORAGE_BUCKET,
messagingSenderId: process.env.MESSAGING_SENDER_ID,
}
const rrfConfig = {
userProfile: 'users',
// useFirestoreForProfile: true, // Firestore for Profile instead of Realtime DB
// attachAuthIsReady: true
}
// Initialize Cloud Firestore through Firebase
export default function configureStore(initialState = {}, history) {
firebase.initializeApp(firebaseConfig)
// Initialize Firestore with timeshot settings
firebase.firestore()
// firebase.firestore().settings({ timestampsInSnapshots: true })
// Create the store with two middlewares
// 1. sagaMiddleware: Makes redux-sagas work
// 2. routerMiddleware: Syncs the location/URL path to the state
const middlewares = [sagaMiddleware, routerMiddleware(history)]
const enhancers = [
applyMiddleware(...middlewares),
// reactReduxFirebase(config), // enhancing our store with these packages
// reduxFirestore(config)
]
// If Redux DevTools Extension is installed use it, otherwise use Redux compose
/* eslint-disable no-underscore-dangle, indent */
const composeEnhancers =
process.env.NODE_ENV !== 'production' &&
typeof window === 'object' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
: compose
/* eslint-enable */
const createStoreWithFirebase = compose(
reactReduxFirebase(firebase, rrfConfig), // firebase instance as first argument
reduxFirestore(firebase),
)(createStore)
const store = createStoreWithFirebase(
createReducer(),
fromJS(initialState),
composeEnhancers(...enhancers),
)
// Extensions
store.runSaga = sagaMiddleware.run
store.injectedReducers = {} // Reducer registry
store.injectedSagas = {} // Saga registry
// Make reducers hot reloadable, see http://mxs.is/googmo
/* istanbul ignore next */
if (module.hot) {
module.hot.accept('./reducers', () => {
store.replaceReducer(createReducer(store.injectedReducers))
})
}
return store
}
I traced and verified my store configuration exactly to make sure all steps present in the documentation are configured correctly in my configuration.
My createReducer funciton is in a seperate file and you can see that I added the firebaseReducer and firebaseReducer correctly.
import { combineReducers } from 'redux-immutable'
import { connectRouter } from 'connected-react-router/immutable'
import { firebaseReducer } from 'react-redux-firebase'
import { firestoreReducer } from 'redux-firestore'
import history from 'utils/history'
import languageProviderReducer from 'containers/LanguageProvider/reducer'
export default function createReducer(injectedReducers = {}) {
const rootReducer = combineReducers({
firebase: firebaseReducer,
firestore: firestoreReducer,
language: languageProviderReducer,
...injectedReducers,
})
// Wrap the root reducer and return a new root reducer with router state
const mergeWithRouterState = connectRouter(history)
return mergeWithRouterState(rootReducer)
}
My redux store contains the firestore and firebase and it is injected into the component props.
What does not work is the use of connectFirestore HoC to automatically retrieve and inject a list of documents in to the component.
This is the error message:
react-dom.development.js?61bb:20266 Uncaught TypeError: Cannot read property 'ordered' of undefined
at Function.eval [as mapToProps] (index.js?d834:49)
at mapToPropsProxy (wrapMapToProps.js?1817:54)
at Function.detectFactoryAndVerify (wrapMapToProps.js?1817:63)
at mapToPropsProxy (wrapMapToProps.js?1817:54)
at handleFirstCall (selectorFactory.js?805c:37)
at pureFinalPropsSelector (selectorFactory.js?805c:85)
at Object.runComponentSelector [as run] (connectAdvanced.js?48b8:43)
at Connect.initSelector (connectAdvanced.js?48b8:195)
at new Connect (connectAdvanced.js?48b8:136)
at constructClassInstance (react-dom.development.js?61bb:11315)
(Snipped from my code which is the example 1 in documentation):
export default compose(
firestoreConnect(['todos']), // or { collection: 'todos' }
connect((state, props) => ({
todos: state.firestore.ordered.todos
}))
)(Todos)
I inspected the state variable and it does contain the firestore attribute. This attribute contains a number of functions, as expected, but it is missing the query results under "ordered", which is undefined.
I have tried all different ways to use firestoreconnect e.g. using a Class-based component, using a query with parameters, etc. and all give the same error.
My Firebase project is configured correct as I am able to create documents inside collections. A todos collection for testing purposes is present as well containing 2 documents.
I have come across this post, which mentions the following:
If you just upgraded to React-Redux v6, it's because react-redux-firebase is not compatible with v6.
See https://github.com/prescottprue/react-redux-firebase/issues/581 for details.
This does not apply to me because I am using react-redux version 5. Here are the versions I am using:
"firebase": "^5.10.1",
"react-redux": "^5.0.7",
"react-redux-firebase": "^2.2.6",
"redux": "^4.0.1",
"redux-firestore": "^0.7.3",
I have spent a significant amount of time on this. Like I said, using firestore to add new data to collections works fine. It is just this HoC business that is failing no matter how i approach the solution.
any help would be appreciated.
Never solved this. I guess it is related to incompatible versions. What I ended up doing is download v4 of react-boilerplate and set up v3 react-redux-firebase which uses the Context API as opposed to store enhancers. Now works very well.
I'm developing a React-native app, using Redux and Firebase.
My Firebase database is denormalized, so it looks like:
users:
user_uid:
my_posts: [ post_key1, post_key2 ]
posts
post_key1: { post_details }
post_key2: { post_details }
How should I fetch data asynchronously and dispatch posts data to Redux store?
I know about Firebase methods .on('value') and .once('value'), but I'm not able to write a proper async function/thunk without generating issues.
If you are using react-redux-firebase to integrate redux with Firebase, the v2.0.0 docs show using react-native with examples for using either native-modules through react-native-firebase or the JS SDK.
With the structure you have shown, it may also be helpful for you to use populate to easily load posts automatically when loading users.
If you have the users uid on the post object under owner, you could do something like:
Home.js
import { compose } from 'redux'
import { connect } from 'react-redux'
import { firebaseConnect, populate } from 'react-redux-firebase'
const populates = [
{ child: 'owner', root: 'users' } // replace owner with user object
]
const enhance = compose(
firebaseConnect([
// passing populates parameter also creates all necessary child queries
{ path: 'posts', populates }
]),
connect(({ firebase }) => ({
// populate original from data within separate paths redux
posts: populate(firebase, 'posts', populates),
}))
)
const SomeComponent = ({ posts }) => <div>{JSON.stringify(posts, null, 2)}</div>
export default enhance(SomeComponent)
App.js
import { createStore, combineReducers, compose } from 'redux'
import { connect } from 'react-redux'
import { reactReduxFirebase, firebaseReducer } from 'react-redux-firebase'
import firebase from 'firebase'
import Home from './Home' // code above
const firebaseConfig = {} // config from firebase console
// react-redux-firebase config
const rrfConfig = {
userProfile: 'users' // automatically manage profile
}
// initialize firebase instance
firebase.initializeApp(config) // <- new to v2.*.*
// Add reduxReduxFirebase enhancer when making store creator
const createStoreWithFirebase = compose(
reactReduxFirebase(firebase, rrfConfig)
)(createStore)
// Add Firebase to reducers
const rootReducer = combineReducers({
firebase: firebaseStateReducer
})
// Create store with reducers and initial state
const initialState = {}
const store = createStoreWithFirebase(rootReducer, initialState)
const App = () => (
<Provider store={store}>
<Home />
</Provider>
);
ReactDOM.render(<App/>, document.querySelector('#app'));