Finding object in array initial state React-Redux app - react-redux

I am currently working on an app that do not support server rendering, so i have to have an empty state when the client loads the app. Then a fetch is dispatched and the state is soon updated.
I have a component (some kind of editor) which should find an object based on a url-parameter. My mapStateToProps function looks something like this:
const mapStateToProps = (state, ownProps) => {
return {
book: state.library.list.find(function(book){ return book.id === ownProps.params.book_id})
}
}
this.props.book is undefined when the component runs getInitialState, so it does not get the update when the state is fetched. I get the following error in the console of my browser when the component loads:
TypeError: undefined is not an object (evaluating 'this.props.book.title')
From there on the editor remains empty, even when the state is received from the server later on.
Any idea of how i can solve this problem?

What you need to do is put a watch when you are loading data from this.props.book to not be an undefined
if(this.props.book) {
//do something with this.props.book
}

Solution: Preload the store on the server
I finally found an answer to my problem. What i did, was to check upon every request if the user was logged in (through JSON web tokens). If the user was logged in, i preloaded the state and sent it with the first request (assigned it to window._PRELOADED_STATE_ with a script tag in the response string).
Then i also added two lines on the client code
const preloadedState = window.__PRELOADED_STATE__
const store = createStore( rootReducer, preloadedState, applyMiddleware(apiMiddleware, thunkMiddleware) );
then the store was updated before the editor component asked for it.
This solution is based on some ideas introduced at redux's home page http://redux.js.org/docs/recipes/ServerRendering.html

Related

How to track pageviews in RemixJS?

I am building a Remix app, and wanted to record some user analytics in my database based on what page the user was viewing. I also wanted to do so on a route by route basis, rather than just simply the raw URL.
For example: I wanted to know "user viewed URL /emails/123" as well as "user viewed route /emails/$emailId"
This problem could be generalized as "I want to run a piece of server code once per user navigation"
For my tracking I'm assuming users have javascript enabled in their browser.
Solutions I tried:
Record in the loader
This would be something like:
export const loader: LoaderFunction = async ({ request, params }): Promise<LoaderData> => {
myDb.recordPageVisit(request.url);
}
This doesn't work because the loader can be called multiple times per page visit (eg. after an action is run)
It's possible that there's some value hidden in the request parameter that tells us whether this is an initial page load or if it's a later visit, but if so I couldn't find it, including when I examined the raw HTTP requests.
It's also annoying to have to put this code inside of every loader.
Record the URL in the node code
I'm using #remix-run/node as my base remix server, so I have the escape hatch of setting up node middleware, and I thought that might be a good answer:
const app = express();
app.use((req, res, next) => {
if (req.url.indexOf("_data") == -1) {
myDb.recordPageVisit(req.url);
}
next();
});
I tried ignoring routes with _data in them, but that didn't work because remix is being efficient and when the user navigates, it is using an ajax-y call to only get the loaderData rather than getting the full rendered page from the server. I know this is the behavior of Remix, but I had not remembered it before I went down this path :facepalm:
As far as I can tell it's impossible to stateless-ly track unique pageviews (ie based purely on the current URL) - you need see the user's previous page as well.
I wondered if referer would allow this to work statelessly, but it appears that the referer is not behaving how I'd hoped: the referer header is already set to the current page in the first loader request for the data for the page. So initial load and load-after-mutation appear identical based on referer. I don't know if this is technically a bug, but it's certainly not the behavior I'd expect.
I ended up solving this by doing the pageview tracking in the client. To support recording this in the DB, I implemented a route that just received the POSTs when the location changed.
The documentation for react-router's useLocation actually includes this exact scenario as an example.
From https://reactrouter.com/docs/en/v6/api#uselocation:
function App() {
let location = useLocation();
React.useEffect(() => {
ga('send', 'pageview');
}, [location]);
return (
// ...
);
}
However, that doesn't quite work in remix - the location value is changed after actions (same text value, but presumably different ref value). So I started saving the last location string seen, and then only report a new pageview when the location string value has changed.
So after adding that stateful tracking of the current location, I landed on:
export default function App() {
// ...other setup omitted...
const [lastLocation, setLastLocation] = useState("");
let location = useLocation();
const matches = useMatches();
useEffect(() => {
if (lastLocation == location.pathname) {
return;
}
// there are multiple matches for parent route + root route, this
// will give us the leaf route
const routeMatch = matches.find((m) => m.pathname == location.pathname);
setLastLocation(location.pathname);
fetch("/api/pageview", {
body: JSON.stringify({
url: location.pathname,
// routeMatch.id looks like: "/routes/email/$emailId"
route: routeMatch?.id }),
method: "POST",
}).then((res) => {
if (res.status != 200) {
console.error("could not report pageview:", res);
}
});
}, [location]);
The matches code is not necessary for tracking just raw URLs, but I wanted to extract the route form (eg /emails/$emailId), and matches.id is a close match to that value - I strip "routes/" serverside. Matches docs: https://remix.run/docs/en/v1/api/remix#usematches
Client side pageview tracking is a bit annoying since clients are flaky, but for the current remix behavior I believe this is the only real option.
Did it a different way for routes, remix is funny cuz of the whole parent route thing * so I use a route logger
Beans.io
https://www.npmjs.com/package/beansio

Trigger refresh of related components

I have a React-Redux app which displays a dashboard page when the user first logs on. The dashboard displays a number of statistics/graphs etc. which are components most (not all) of which are filtered based on a state site value. This site value is initially set when the user logs on and is derived from their profile.
The components mostly follow the pattern of using the componentDidMount lifecycle event to call a thunk method. Within the thunk method the site value is retrieved from redux state and passed in the database query. Reducer then adds results to state. Standard stuff. The redux state is fairly flat i.e. the state for the statistics/graphs etc. are not nested under the selected site but are their own objects off the root.
On the dashboard is also a dropdownlist which contains all sites. Initially this is set to the users default site. This dropdown is intended to allow the user to see statistics/graphs for sites other than their default.
What I would like is for all the (site specific) dashboard components to refresh when the user selects a different site from the dropdownlist. The problem I am having is how do I get these components to refresh? or more specifically, how to get their state to refresh.
UPDATE:
I tried two different approaches.
In the thunk action for handling the site change effect (changeSite) I added dispatch calls to each components thunk action. Although this worked I didn't like the fact that the changeSite thunk needed to know about the other components and what action creators to call. i.e. thunk action looked like:
changeSite: (siteId: number): AppThunkAction<any> => async (dispatch) => {
const promiseSiteUpdate = dispatch({ type: 'CHANGE_SITE', siteId });
await Promise.all([promiseSiteUpdate]);
dispatch(AncillariesStore.actionCreators.requestExpiring(siteId));
dispatch(AncillariesStore.actionCreators.requestExpired(siteId));
dispatch(FaultsStore.actionCreators.requestFaults(siteId));
dispatch(AssetsStore.actionCreators.requestAssetsCount(siteId));
dispatch(LicencesStore.actionCreators.requestLicencesCount(siteId));
dispatch(MaintenancesStore.actionCreators.requestMaintenancesCount(siteId));
dispatch(FaultsStore.actionCreators.requestFaultsCount(siteId));
}
Within a dependant component, included the Site value in the component properties, hooked into the componentDidUpdate lifecycle event. Checked if the Site had changed and then called the thunk action for updating the component state. I preferred this approach as it kept the business logic within the component so the changeSite thunk could now become a simple reducer call. An example of a dependent (site faults) component looks like this:
type FaultsProps =
{
faults: FaultsStore.FaultsItem[],
currentSiteId: number
}
& typeof FaultsStore.actionCreators;
const mapStateToProps = (state: ApplicationState) => {
return {
faults: state.faults?.faults,
currentSiteId: state.sites?.currentSiteId
}
}
class FaultyListContainer extends React.PureComponent<FaultsProps> {
public componentDidMount() {
// First load.
this.props.requestFaults(this.props.currentSiteId);
}
public componentDidUpdate(prevProps: FaultsProps) {
// Update state if site has changed.
if(prevProps.currentSiteId !== this.props.currentSiteId) {
this.props.requestFaults(this.props.currentSiteId);
}
}
public render() {
return React.createElement(FaultsList, {faults: this.props.faults});
}
}
export default connect(
mapStateToProps,
FaultsStore.actionCreators
)(FaultyListContainer as any);
Is #2 the best approach? is there a better way?

Vuex error: "Do not mutate vuex store state outside mutation handlers."

I have a button on my website that performs the following action:
this.$store.commit('SET_THREAD_UPDATE', thread)
'thread' is an object consisting of multiple properties and objects and when the function is called there might be only a small change within an object of 'thread' or one of its properties.
'SET_THREAD_UPDATE' is a Vuex mutation and the only one of many that causes problems even though I am not doing anything differently.
const state = {
threadUpdate: {}
}
const mutations = {
SET_THREAD_UPDATE (state, userObj) {
state.threadUpdate = userObj
}
}
When the button that triggers the commit is pressed the first time, everything works like expected. But then, from the second time forward I get two errors and the commit doesn't do anything:
[Vue warn]: Error in callback for watcher "function () { return this._data.$$state }": "Error: [vuex] Do not mutate vuex store state outside mutation handlers." (found in <Root>)
Error: "[vuex] Do not mutate vuex store state outside mutation handlers."
I can't figure out the error. It doesn't make sense because I am using a mutation to change the state. Ideas anyone? Thanks!
Solution to the problem was to use JSON instead of regular JS objects. Not sure why though.

Access store from service (or where I should be accessing the store)?

I'm only a few days in (transitioning from Ember) so please pardon my ignorance.
I have an array of object (profileaccounts) in my store.
I have many different components which are connected to the store and have code like below, sometimes the user is the person logged in, sometimes its a user that is being passed into the component (when someone looks at someone else's profile)
componentDidMount() {
let user = this.props.user;
let account = this.props.profiles.find(function (prof) { return prof.profile.id === user.id });
if (account == null) {
this.props.dispatch(userActions.getProfile(user.id));
}
}
This completely works but I don't want this code replicated over and over again. My gut feeling is that I should always call .getProfile(user.id) and its the job of the actions to determine if the data exist in the local cache (store) or does it needs to be added. If it needs to be added, add it, then either way return it.
Alternatively, maybe the user service (which represents the API and is called by the actions to populate the profiles) is supposed to look locally before it calls the API. Regardless, I don't know how (or if I should) to access the store from the actions or service, only from the connected component.
I haven't seen this scenario in any of the guides I've read about redux/react, so if anyone can include a resource to where I should be looking I'd appreciate it. If I'm totally going about this the wrong way, I'd be happy to know that too.
You can use redux-thunk to access state inside the action
link to thunk: https://github.com/reduxjs/redux-thunk
The code would be like this:
function incrementIfOdd() {
return (dispatch, getState) => {
const { counter } = getState();
if (counter % 2 === 0) {
return;
}
dispatch(increment());
};
}
Using thunk you'll be able to access state inside the action.
Your approach sounds good...You'll start with dispatching an action "dispatch(getProfile(userId))" then inside the action you'll do whatever you want and when you finally have the data you want to put in the store dispatch another action "dispatch(storeUserProfile(profile))" which will put the data in the store via reducer.

Reload page with new context in express

I have a page that lists events, in which admins are can delete individual items with an AJAX call. I want to reload the page when an event is deleted, but I am having trouble implementing it with my current understanding of express' usual req, res, and next.
Here is my current implementation (simplified):
Simple jQuery code:
$(".delete").click(function(e){
$.post("/events/delete",{del:$(this).val()})
})
in my routes file:
function eventCtrl(req,res){
Event.find({}).exec(function(err,events){
...
var context = {
events:events,
...
}
res.render('events',context);
});
}
function deleteCtrl(req,res,next){
Event.findById(req.param("del")).exec(function(err,event){
// delete my event from google calendar
...
event.remove(function(err){
...
return next();
});
});
}
app.get('/events',eventCtrl);
app.post('/events/delete',deleteCtrl,eventCtrl);
When I make a post request with AJAX all the req handlers are called, the events are deleted successfully, but nothing reloads. Am I misunderstanding what res.render() does?
I have also tried using a success handler in my jQuery code when I make the post request, with a res.redirect() from deleteCtrl, but my context is undefined in that case.
on the client side, you are using
$(".delete").click(function(e){
$.post("/events/delete",{del:$(this).val()})
})
this code does not instruct the browser to do anything when the response from the post is received. So nothing visible happens in the browser.
You problem is not located server side ; the server answers with the context object. You are simply not doing anything with this answer.
Try simply adding a successHandler.
Generally speaking this would not be a best practice. What you want to do is reconcile the data. If the delete is successful, then just splice the object out of the array it exists in client-side. One alternative would be to actually send back a refreshed data set:
res.json( /* get the refreshed data set */ );
Then client-side, in the callback, you'd actually just set the data source(s) back up based on the result:
... myCallback(res) {
// refresh the data source(s) from the result
}

Resources