Props in component only retrieved on reload, not from navigation - reactjs

I have an action that sends a Firebase query that relies on store data - specifically the uid.
The uid IS populated and stored upon user sign-in, which is stored in an auth object.
In this component, I'm retrieving the uid, firing the action, receiving the payload, updating the state with a reducer, and rendering the component with the data.
I've mapped the state to props thus (initially this.props.uid is always undefined):
function mapStateToProps(state, props) {
return {
uid: state.auth.uid,
componentData: state.thisComponent.componentData
}
}
I check the uid thus (written before render()):
componentWillReceiveProps(nextProps) {
if (nextProps.uid !== this.props.uid) {
this.props.actions.getComponentData(nextProps.uid)
}
}
The action and query work, but only if the component is refreshed - not if the component is simply navigated to. Additionally, I'm using redux-persist to hydrate the store and persist its state:
**index.js**
... combineReducers etc
let store = createStore(rootReducer, applyMiddleware(thunk), autoRehydrate());
persistStore(store)
How do I make sure that componentWillReceiveProps always dispatches the action and the payload is rendered - whether the page is opened from a different link or when it is refreshed?

componentWillReceiveProps is not run when the component is mounted, it is only run, like you say, after initialization and when the component will receive a new set of props.
I believe you are looking for componentWillMount, anything put in here will run before the component is mounted, meaning, this life-cycle event is invoked immediately before the render method.
componentWillMount(props) {
props.actions.getComponentData(props.uid)
}
Take a look at http://busypeoples.github.io/post/react-component-lifecycle/ for more information
Depending on your use case you might need it on both.

Related

In Next.js, how can I update React Context state with data from getServerSideProps?

I'm having a lot of trouble learning to properly load data into state in my todo app.
I have a next.js page component pages/index.tsx where I load data from my API via getServerSideProps and return it as a page prop called tasksData.
The tasksData is being returned properly and I can access them in my page component just fine via prop destructuring: const Home = ({ tasksData }: Home) => { }
I also have a React Context provider in _app.tsx called BoardProvider. This stores state for my task board, and employs useReducer() from the React Context API to update this state in context consumers, such as pages/index.tsx.
The challenge I am facing is how to make my UI's "source of truth" the state stored in my context provider (eg. const { { tasks }, dispatch } = useBoard();, rather than the page page props returned from my API (eg. the tasksData prop).
One approach I considered was to simply load the data in getServerSideProps and then set the state via a dispatched action in a useEffect hook:
useEffect(() => {
// On first render only, set the Context Provider state with data from my page props.
dispatch({ type: TaskAction.SET_TASKS, payload: tasksData });
});
However, this doesn't seem to be working because sometimes tasksData is undefined, presumably because Next.js has not yet made it available on page mount.
Another suggestion I heard was to fetch the data and pass it as pageProps to my Context Provider in _app.tsx. I believe this means using getInitialProps() in _app.tsx so that my provider's initial state is populated by my API. However, this disabled static optimization and other useful features.
Can anyone help me out with some pseudocode, documentation, or examples of how to use getServerSideProps in combination with React Context API?
Couple of points:
getServerSideProps should be invoked before the page is even rendered. So theoretically your tasksData is undefined is a bug! You can't have a server data to be unavailable unless you really really intend to have that happen in the first place.
Assuming getServerSideProps is always returning the right data, but you want to use your own data to override it. In your context, you can have this logic.
const Home = ({ tasksData }) => {
const value = { tasksData: {
// let me override it
}}
return (
<Context.Provider value={value}>
...
<Context.Provider>
)
}
If you have the context provided under a page, the above code is all you need. But if your context is provided in a root (parent of a page), you can still add the above code to re-provide the same context again with overridden value. Because this is how a context is designed, read https://javascript.plainenglish.io/react-context-is-a-global-variable-b4b049812028 for more info.

Using Redux, can any component get and set (by action) any data in the store?

Using Redux, is it true that any component and sub-component on the page get all data of the one and only store, and be able to send out any action at all, even if it is not meant for that component to send out?
Can all the components use
const store = createStore(mainReducer);
let state = store.getState();
and be able to see all states of the whole app? Can any component dispatch any action all all? So for example, if there is Counter component and a Comment component, can the Comment component accidentally send out a "INCREASE_COUNT" action?
Using Redux any component "can" access any data in the store, but for the access you have to 'connect' it with the store. When you 'connect' you also specify a map to which part you want this component to access. That's how you are in control, it only gets access to what you want only.
The same goes for actions. You have to map the actions also - which component can dispatch which action, when your 'connect' to the store.
Check this out for more info - https://redux.js.org/basics/usage-with-react
To most part of your question, it seems the answer is Yes.
Yes, the components can access the whole store ( one it has subscribed to ) and can dispatch actions when needed. I do not think there is any way you can put action/store behind some restrictions.
can the Comment component accidentally send out an "INCREASE_COUNT" action? Yes if you try to dispatch it again from the child component.
If you could add any specific example you have to ask, I can add more to my answer.
I hope it helps you !
" every component has access to the store" is wrong, it is like this " every component has access to the state and actions in the store that you "the developer" specify.
for a component to able to access the store, you need to wrap it in the connection function like so
import { connect } from "react-redux";
// Your component
export default connect(mapStateToProps, dispatchActionToProps);
// the component will only have access to the store props and actions that you specify
// in mapStateToProps and dispatchActionToProps
const mapStateToProps = state => {
return {
// state is the result of combineReducers
// whatevery key the component needs you can specify here
};
}
const dispatchActionToProps = dispatch => {
return {
// your store actions
};
}

How can you use a React component's local state inside of the mapStateToPropsMethod?

I have a react component which creates some local state: a query identifier. This query identifier is created when the component is created and is not/should not be passed in from a consumer of the component. This state is only used within the component instance and is not shared amongst anything else -- as such, it feels like it is appropriate to be local state in the component and not stored in the redux store.
The react component dispatches an async action via redux and when the action completes, the result is stored in a collection in the redux state that is indexed by the aforementioned query id.
In the React components mapStateToProps function, I want to retrieve the query results from the query results collection in the redux state. However, the query id for the component is only in the local state of the component instance and thus not obviously accessible from mapStateToProps (since mapStateToProps is not a member of the component class and does not obviously have a way to access the local state of that instance).
Is there an established way of accessing the local state of a React component when inside of mapStateToProps? I know I could store that local state in redux (so it's available in mapStateToProps), but it seems like it would be unnecessary since the state is really local to each component instance. The ownProps parameter looked interesting but it seems like that requires consumers of my React component to pass a value in the props when the component is instantiated, which is not necessary (confusing, even) here.
Is there an expected way to do this? Or have I designed my component wrong such that I shouldn't be trying to use component local state to properly map redux state to component props?
Thanks!
It isn't possible to access a component state in mapStateToProps method since its used within connect which is a wrapper over the component.
You can however get those values within mapStateToProps if you either lift the state up to the parent of the component and then pass it as props to the child. In such a case, you can access the value in mapStateToProps using the ownProps argument. However its unnecessary to move the state to the parent if its just local to this particular component. Similar is a case if the state is moved to redux store.
A better way of handling such a scenario is to pass the entire set of value from mapStateToProps on to the component and then you implement a memoized method that returns the result based on the data and the query id
Ex:
const mapStateToProps = (state, props) => {
return {
collection: resultCollectionSelector(state, props)
}
}
and in components render method(you can also do it in componentDidUpdate and set the value to state is you require to use it in method apart from render)
constructor(props) {
super(props);
this.getMemoizedFilteredResult = _.memoize(this.getFilteredResult);
}
getFilteredResult = (collection, query) => {
// process and return result
}
render() {
const queryResult = this.getMemoizedFilteredResult(this.props.collection, this.state.query);
...
}

how to communicate between a root React component and the rest of the app using Redux?

I have a component rendered on the root of my app that handles alerts.
I want to be able to pass different messages to that component from anywhere in my app and get the action that was selected by the user (Accept or Decline for example).
Since that component is not a child of the other components, I can't pass a callback to it as a prop.
I don't want to "instance" that component on every component that needs to render it mainly because of markup organization.
What is a good pattern to achieve this? Should I pass the actions that need to be dispatched from the Alert component in the invoking action itself?
If you are using Redux. You just need to use a function mapStateToProps which returns an object and a helper function connect to conenect the store with your components. Then the components will receive the state as its own props when the state in Redux store has been changed. i.e. You acted an action and reducers returned (changed) the new values in the store, the components which have been connected will be rendered again since it (thinks itself) received new props.
For example, your Redux store (singleton) is
{
alerts: [{
id: 1001,
message: "Hello",
userAct: "Accepted",
custType: "fromCompA",
},
{
id: 1002,
message: "World",
userAct: "Declined"
custType: "fromCompB",
cb: function(){}
}],
others: {},
}
mapStateToProps
const mapStateToProps = state => {
return {
alerts: state.alerts || []
}
}
connect
export default connect(mapStateToProps, {
addAlert,
})(YourComponent)
UPDATE
If you want to do the different actions according to which component invoked it, you can add a custom type or a callback in your store's object.
Then your can use an action creator with a custom type like this (You can put custType into your param object, all depends on your design.)
const addAlert = (paramIsObjContainsCustType, param2, custType) => (
{
type: "YOUR_ACTION_TYPE",
payload: {
message: param2,
custType,
}
)
And your component can get an alert object and object.custType (in the example). Then you can do stuff according to the value of it.
By #azium 's comments below, the callback way may miss some features of Redux like time travel. Therefore, to leave a metadata like custType in the store and react according to it should be a better way to do what you want.
And keep functions in the store might also cause some issues when we want to serialize the store. functions may be null when you do the serialization.

Firing Redux actions in response to route transitions in React Router

I am using react-router and redux in my latest app and I'm facing a couple of issues relating to state changes required based on the current url params and queries.
Basically I have a component that needs to update it's state every time the url changes. State is being passed in through props by redux with the decorator like so
#connect(state => ({
campaigngroups: state.jobresults.campaigngroups,
error: state.jobresults.error,
loading: state.jobresults.loading
}))
At the moment I am using the componentWillReceiveProps lifecycle method to respond to the url changes coming from react-router since react-router will pass new props to the handler when the url changes in this.props.params and this.props.query - the main issue with this approach is that I am firing an action in this method to update the state - which then goes and passes new props the component which will trigger the same lifecycle method again - so basically creating an endless loop, currently I am setting a state variable to stop this from happening.
componentWillReceiveProps(nextProps) {
if (this.state.shouldupdate) {
let { slug } = nextProps.params;
let { citizenships, discipline, workright, location } = nextProps.query;
const params = { slug, discipline, workright, location };
let filters = this._getFilters(params);
// set the state accroding to the filters in the url
this._setState(params);
// trigger the action to refill the stores
this.actions.loadCampaignGroups(filters);
}
}
Is there a standard approach to trigger actions base on route transitions OR can I have the state of the store directly connected to the state of the component instead of passing it in through props? I have tried to use willTransitionTo static method but I don't have access to the this.props.dispatch there.
Alright I eventually found an answer on the redux's github page so will post it here. Hope it saves somebody some pain.
#deowk There are two parts to this problem, I'd say. The first is that componentWillReceiveProps() is not an ideal way for responding to state changes — mostly because it forces you to think imperatively, instead of reactively like we do with Redux. The solution is to store your current router information (location, params, query) inside your store. Then all your state is in the same place, and you can subscribe to it using the same Redux API as the rest of your data.
The trick is to create an action type that fires whenever the router location changes. This is easy in the upcoming 1.0 version of React Router:
// routeLocationDidUpdate() is an action creator
// Only call it from here, nowhere else
BrowserHistory.listen(location => dispatch(routeLocationDidUpdate(location)));
Now your store state will always be in sync with the router state. That fixes the need to manually react to query param changes and setState() in your component above — just use Redux's Connector.
<Connector select={state => ({ filter: getFilters(store.router.params) })} />
The second part of the problem is you need a way to react to Redux state changes outside of the view layer, say to fire an action in response to a route change. You can continue to use componentWillReceiveProps for simple cases like the one you describe, if you wish.
For anything more complicated, though, I recommending using RxJS if you're open to it. This is exactly what observables are designed for — reactive data flow.
To do this in Redux, first create an observable sequence of store states. You can do this using rx's observableFromStore().
EDIT AS SUGGESTED BY CNP
import { Observable } from 'rx'
function observableFromStore(store) {
return Observable.create(observer =>
store.subscribe(() => observer.onNext(store.getState()))
)
}
Then it's just a matter of using observable operators to subscribe to specific state changes. Here's an example of re-directing from a login page after a successful login:
const didLogin$ = state$
.distinctUntilChanged(state => !state.loggedIn && state.router.path === '/login')
.filter(state => state.loggedIn && state.router.path === '/login');
didLogin$.subscribe({
router.transitionTo('/success');
});
This implementation is much simpler than the same functionality using imperative patterns like componentDidReceiveProps().
As mentioned before, the solution has two parts:
1) Link the routing information to the state
For that, all you have to do is to setup react-router-redux. Follow the instructions and you'll be fine.
After everything is set, you should have a routing state, like this:
2) Observe routing changes and trigger your actions
Somewhere in your code you should have something like this now:
// find this piece of code
export default function configureStore(initialState) {
// the logic for configuring your store goes here
let store = createStore(...);
// we need to bind the observer to the store <<here>>
}
What you want to do is to observe changes in the store, so you can dispatch actions when something changes.
As #deowk mentioned, you can use rx, or you can write your own observer:
reduxStoreObserver.js
var currentValue;
/**
* Observes changes in the Redux store and calls onChange when the state changes
* #param store The Redux store
* #param selector A function that should return what you are observing. Example: (state) => state.routing.locationBeforeTransitions;
* #param onChange A function called when the observable state changed. Params are store, previousValue and currentValue
*/
export default function observe(store, selector, onChange) {
if (!store) throw Error('\'store\' should be truthy');
if (!selector) throw Error('\'selector\' should be truthy');
store.subscribe(() => {
let previousValue = currentValue;
try {
currentValue = selector(store.getState());
}
catch(ex) {
// the selector could not get the value. Maybe because of a null reference. Let's assume undefined
currentValue = undefined;
}
if (previousValue !== currentValue) {
onChange(store, previousValue, currentValue);
}
});
}
Now, all you have to do is to use the reduxStoreObserver.js we just wrote to observe changes:
import observe from './reduxStoreObserver.js';
export default function configureStore(initialState) {
// the logic for configuring your store goes here
let store = createStore(...);
observe(store,
//if THIS changes, we the CALLBACK will be called
state => state.routing.locationBeforeTransitions.search,
(store, previousValue, currentValue) => console.log('Some property changed from ', previousValue, 'to', currentValue)
);
}
The above code makes our function to be called every time locationBeforeTransitions.search changes in the state (as a result of the user navigating). If you want, you can observe que query string and so forth.
If you want to trigger an action as a result of routing changes, all you have to do is store.dispatch(yourAction) inside the handler.

Resources