I am implementing server-side rendering using redux-saga.
I am following the "real world" example provided in the redux-saga repository.
node.js entry-point uses react.js renderToString to render the application.
rendering the application triggers componentWillMount, which dispatches actions GET_GEOLOCATION and GET_DATE. These async actions will resolve with SET_GEOLOCATION and SET_DATE.
renderToString finishes rendering the application; END action terminates the saga listeners
The problem is that SET_GEOLOCATION and SET_DATE themselves are used to put a new action GET_MOVIES. However, by the time the SET_GEOLOCATION and SET_DATE are called, the saga listeners are no longer active (we terminated it after renderToString). Therefore, while GET_MOVIES will be dispatched, the GET_MOVIES action will not be picked and SET_MOVIE will never happen.
Server code:
app.get('*', (req, res) => {
const history = createMemoryHistory({
initialEntries: [
req.url
]
});
const store = configureStore(undefined, history);
const context = {};
const rootComponent = <Provider store={store}>
<StaticRouter context={context} location={req.url}>
<Route component={RootRoute} />
</StaticRouter>
</Provider>;
store
.runSaga(rootSaga).done
.then(() => {
const body = renderToString(rootComponent);
const response = renderHtml(body, store);
res
.send(response);
})
.catch((error) => {
res
.status(500)
.send(error.message);
});
// Force componentWillMount to issue saga effects.
renderToString(rootComponent);
store.close();
});
Sagas:
const watchNewSearchCriteria = function *(): Generator<*, *, *> {
yield takeLatest([
SET_GEOLOCATION,
SET_DATE
], function *() {
const {
coordinates,
date
} = yield select((state) => {
return {
coordinates: state.movieEventsView.location ? state.movieEventsView.location.coordinates : null,
date: state.movieEventsView.date
};
});
if (!coordinates || !date) {
return;
}
yield put(getMovies({
coordinates,
date
}));
});
};
const watchGetMovies = function *() {
yield takeLatest(GET_MOVIES, function *(action) {
const result = yield call(getMovies, action.payload);
yield put(setMovies(result));
});
};
How to delay store.close until after there are no sagas that are in the state other than take?
How to delay store.close until after there are no sagas that are in the state other than take?
To answer my own question, I need to observe resolution of anything thats been put. I can do this using the Saga Monitor.
Saga Monitor can be configured at the time of creating the redux-saga middleware. For our use case, it needs to track whenever an action has been put and remove it from the index when it has been resolved/ rejected/ cancelled.
const activeEffectIds = [];
const watchEffectEnd = (effectId) => {
const effectIndex = activeEffectIds.indexOf(effectId);
if (effectIndex !== -1) {
activeEffectIds.splice(effectIndex, 1);
}
};
const sagaMiddleware = createSagaMiddleware({
sagaMonitor: {
effectCancelled: watchEffectEnd,
effectRejected: watchEffectEnd,
effectResolved: watchEffectEnd,
effectTriggered: (event) => {
if (event.effect.CALL) {
activeEffectIds.push(event.effectId);
}
}
}
});
We need to access this from the consumer of the store, therefore I assign activeEffectIds to the store instance:
store.runSaga = sagaMiddleware.run;
store.close = () => {
store.dispatch(END);
};
store.activeEffectIds = activeEffectIds;
Then instead of synchronously stopping the saga...
renderToString(rootComponent);
store.close();
we need to delay store.close until store.activeEffectIds.length is 0.
const realDone = () => {
setImmediate(() => {
if (store.activeEffectIds.length) {
realDone();
} else {
store.close();
}
});
};
// Force componentWillMount to issue saga effects.
renderToString(rootComponent);
realDone();
Now store.close is called only when all the asynchronous effects are resolved.
Related
Since Nuxt's fetch hooks cannot run in parallel, I needed a way to cancel requests done in fetch hook when navigating to some other route so users don't have to wait for the first fetch to complete when landed on the homepage navigated to some other. So I found this approach: How to cancel all Axios requests on route change
So I've created these plugin files for Next:
router.js
export default ({ app, store }) => {
// Every time the route changes (fired on initialization too)
app.router.beforeEach((to, from, next) => {
store.dispatch('cancel/cancel_pending_requests')
next()
})
}
axios.js
export default function ({ $axios, redirect, store }) {
$axios.onRequest((config) => {
const source = $axios.CancelToken.source()
config.cancelToken = source.token
store.commit('cancel/ADD_CANCEL_TOKEN', source)
return config
}, function (error) {
return Promise.reject(error)
})
}
and a small vuex store for the cancel tokens:
export const state = () => ({
cancelTokens: []
})
export const mutations = {
ADD_CANCEL_TOKEN (state, token) {
state.cancelTokens.push(token)
},
CLEAR_CANCEL_TOKENS (state) {
state.cancelTokens = []
}
}
export const actions = {
cancel_pending_requests ({ state, commit }) {
state.cancelTokens.forEach((request, i) => {
if (request.cancel) {
request.cancel('Request canceled')
}
})
commit('CLEAR_CANCEL_TOKENS')
}
}
Now this approach works fine and I can see requests get canceled with 499 on route change, however, it is flooding my devtools console with "Error in fetch()" error. Is there some preferred/better way to do this?
Example of fetch hook here:
async fetch () {
await this.$store.dispatch('runs/getRunsOverview')
}
Example of dispatched action:
export const actions = {
async getRunsOverview ({ commit }) {
const data = await this.$axios.$get('api/frontend/runs')
commit('SET_RUNS', data)
}
}
Edit: I forgot to mention that I'm using fetch here with fetchOnServer set to False to display some loading placeholder to users.
The main problem is the flooded console with error, but I can also see that it also enters the $fetchState.error branch in my template, which displays div with "Something went wrong" text before route switches.
Edit 2:
Looked closer where this error comes from and it's mixin file fetch.client.js in .nuxt/mixins directory. Pasting the fetch function code below:
async function $_fetch() {
this.$nuxt.nbFetching++
this.$fetchState.pending = true
this.$fetchState.error = null
this._hydrated = false
let error = null
const startTime = Date.now()
try {
await this.$options.fetch.call(this)
} catch (err) {
if (process.dev) {
console.error('Error in fetch():', err)
}
error = normalizeError(err)
}
const delayLeft = this._fetchDelay - (Date.now() - startTime)
if (delayLeft > 0) {
await new Promise(resolve => setTimeout(resolve, delayLeft))
}
this.$fetchState.error = error
this.$fetchState.pending = false
this.$fetchState.timestamp = Date.now()
this.$nextTick(() => this.$nuxt.nbFetching--)
}
Have also tried to have everything using async/await as #kissu suggested in comments but with no luck :/
In the redux, i have a (count, remote Count)
I set it by default 0 for both!
The main idea is comparing if the count equals the remote count I want to dispatch an action 'true/false' that Locks the App
In home screen get a remote count from API then save it in redux store, it's saved well
but I have an if statement that checks if count == remote count I lock the app
So this statement invokes before I save remote count I guess although I add it in then()
Home screen
getRemoteCount = async () => {
try {
let response = await API.get('/number/subsribtion');
let remoteCount = response.data.number;
this.props.saveRemoteCount(remoteCount); // it's saved the remote count!
} catch (err) {
console.log(err);
}
};
componentDidMount() {
const {remoteCount, count} = this.props;
this.getRemoteCount().then(() => {
if (count == remoteCount) {
console.log('count', count);
console.log('remoteCount', remoteCount);//it's log 0!! so the next line invoke!
this.props.isAppLock(true);
}
});
}
Use render to get updated count. componentDidMount run when component mounts for the first time. Save the count on redux store and mapToState in the component.
class C {
getRemoteCount = async () => {
try {
let response = await API.get("/number/subsribtion");
let remoteCount = response.data.number;
this.props.saveRemoteCount(remoteCount); // it's saved the remote count!
} catch (err) {
console.log(err);
}
};
componentDidMount() {
this.getRemoteCount();
}
render() {
const { remoteCount, count } = this.props;
if (count == remoteCount) {
console.log("count", count);
console.log("remoteCount", remoteCount); //it's log 0!! so the next line invoke!
this.props.isAppLock(true);
}
}
}
You can use the dispatch async function using redux thunk.
Install redux thunk.
Use as a middleware inside store.
import thunk from 'redux-thunk';
import rootReducer from './reducers';
const store = createStore(rootReducer, applyMiddleware(thunk));
Use it for dispatch async actions.
return async dispatch=>{ let res= await authLogin(data)}
I'm creating an application where users can create and share notes.
To share each other's notes users have to send requests to specific users.
The requests are fetched whenever home is loaded.
However, requests is a context since it is also consumed in the toolbar and requests page to show the presence of the requests
When I'm using setRequsts method of the context to set all the requests after home loads, the fetch goes into an infinite loop of /noteand /me URLs, since the setRequests method is also provided in the dependency array of useEffect
When removed, useEffect show missing dependencies. What's the work around?
const {setRequests } = useContext(RequestsContext)
const [notes, setNotes] = useState([])
const [fetched, setFetched] = useState('')
const { isAuthenticated } = props
const {page}=useContext(PageContext)
const [sortBy,setSortBy]=useState('latest')
useEffect(() => {
const fetch = async () => {
try {
let url = 'http://192.168.56.1:5000/api/v1/note', p, sort
if (page) p = `?page=${page}&limit=12`
if (sortBy === 'latest') {
sort=''
} else if (sortBy === 'most_liked') {
sort='&sort=likes'
}
const res = await Axios.get(url+p+sort)
setNotes(res.data.data)
if (res.data.data.length > 0) {
setFetched('Y')
} else {
setFetched('N')
}
} catch (err) {
console.log(err)
} finally {
if (isAuthenticated) {
const fetch = async () => {
const res = await axios.get(`user/me`)
if (res.data.data.createdPosts.length > 0) {
const arr = res.data.data.createdPosts.map(el => el.request)
console.log(arr)
setRequests(arr)
}
}
fetch()
}
}
}
fetch()
}, [isAuthenticated, /* setRequests, */ page, sortBy])
The problem is that the context provides a technically different setRequests function on each render (that have a different address). This causes useEffect to fire on each render.
To work around this, you could wrap setRequests in a useCallback() hook, like so:
// ...
const wrappedSetRequests = useCallback(setRequests, []);
// ...
useEffect(() => {
// do your stuff using 'wrappedSetRequests' instead of setRequests.
}, [ wrappedSetRequests /*...*/ ]);
I am creating a small middleware for redux. Since my electron application connects to another database and updates it's state (so that I may see the state of this application on my main application). Here is my basic code:
const BLACKLIST = ['redux-form/FOCUS ', 'redux-form/BLUR'];
export remoteMiddleware => store => next => action => {
const db = MongoClient.connect(process.env.MONGO_URL);
try {
if (!BLACKLIST.includes(action.type)) {
const state = store.getState();
db.collection('state').update(
{ _id: process.env.APP_ID },
{ $set: { state } }
)
}
} finally {
db.close();
}
}
However, I am having a bit of a problem where this is firing TOO often, when it doesn't always need to fire immediately. I would prefer if it fired every n iterations, or perhaps no more than x ms.
Is there a way to throttle a redux middleware so that it will only fire every n times, or such that it will only run every x number of ms?
How about simply
const BLACKLIST = ['redux-form/FOCUS ', 'redux-form/BLUR'];
var lastActionTime = 0;
export remoteMiddleware => store => next => action => {
let now = Date.now();
try {
if (!BLACKLIST.includes(action.type) && now - lastActionTime > 100)
...
} finally() {
lastActionTime = now;
db.close();
}
}
I seem to have a weird bug. I'm currently using Redux isomorphically and am also including redux-thunk as the middleware for async actions. Here's what my store config looks like:
// Transforms state date from Immutable to JS
const transformToJs = (state) => {
const transformedState = {};
for (const key in state) {
if (state.hasOwnProperty(key)) transformedState[key] = state[key].toJS();
}
return transformedState;
};
// Here we create the final store,
// If we're in production, we want to leave out development middleware/tools
let finalCreateStore;
if (process.env.NODE_ENV === 'production') {
finalCreateStore = applyMiddleware(thunkMiddleware)(createStore);
} else {
finalCreateStore = applyMiddleware(
createLogger({transformer: transformToJs}),
thunkMiddleware
)(createStore);
}
// Exports the function that creates a store
export default function configureStore(initialState) {
const store = finalCreateStore(reducers, initialState);
if (module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('.././reducers/index', () => {
const nextRootReducer = require('.././reducers/index');
store.replaceReducer(nextRootReducer);
});
}
return store;
}
The weird part about this is that I don't think there's anything wrong with this file because my createLogger is applied just fine. It logs out all my actions and state, but the minute I return a function instead of an object in an action creator, the execution is lost. I've tried throwing in debugger statements, which never hit and reordering the middleware also doesn't seem to help.
createUser(data) {
// This `debugger` will hit
debugger;
return (dispatch) => {
// This `debugger` will NOT hit, and any code within the function will not execute
debugger;
setTimeout(() => {
dispatch(
AppActionsCreator.createFlashMessage('yellow', 'Works!')
);
}, 1000);
};
},
Has anyone experienced something like this before?
DOH! I wasn't dispatching the action. I was only calling the action creator. Gonna have to get used to that with Redux!
How I thought I was invoking an action:
AppActionCreators.createFlashMessage('some message');
How to actually invoke an action in Redux:
this.context.dispatch(AppActionCreators.createFlashMessage('some message'));
Where dispatch is a method provided by the Redux store, and can be passed down to every child component of the app through React's childContextTypes