I currently initialize the relevant store data (galleries and pieces) on the "Layout" componentDidMount. This data is fetched from an API.
const mapDispatchToProps = {
GetRooms,
GetPieces
}
class Layout extends React.Component {
...
componentDidMount () {
this.props.GetRooms()
this.props.GetPieces()
}
...
render () {
return (
...
<Route path="/Gallery/:roomName" component={Gallery} />
...
)
}
}
I have a separate "Gallery" component that is loaded through a react-router Route inside the Layout component. Gallery's mapStateToProps uses a selector function to filter the requested gallery object and appends a property containing an array of related piece objects.
const GetRoomByName = (state, roomName) => {
const room = state.rooms.all.find(room =>
room.Name === roomName
)
if (!room) return
room.Pieces = state.pieces.all.filter(piece =>
piece.RoomId === room.Id
)
return room
}
const mapStateToProps = (state, { match }) => ({
room: GetRoomByName(state, decodeURIComponent(match.params.roomName))
})
class Gallery extends React.Component {
...
render () {
const { room } = this.props
if (room === undefined) return <h3>Loading...</h3>
return (
...
{room.Pieces.map(piece =>
...
)}
...
)
}
}
When I navigate to this page from the home page, the store is already initialized and the selector executes properly.
But if the route that loads the "Gallery" component is refreshed or directly loaded from the address bar, the data isn't present in the store yet, and when it does load the component does not update. I can run a console log and the room object gets rendered, but the Pieces property doesn't trigger a rerender when it is loaded into the store.
Thank you for your assistance
The problem has to do with how the connect function determines if the component should rerender. connect only rerenders the component if one of the properties in mapStateToProps changes, determined using a shallow comparison. Because it uses a shallow comparison, the component will not rerender unless the reference returned by GetRoomByName changes. To fix this, you should return a new object from GetRoomByName rather than modifying the object that exists in the state (which can lead to other had to diagnose issues down the road as well).
const GetRoomByName = (state, roomName) => {
const room = state.rooms.all.find(room =>
room.Name === roomName
);
if (!room) return
const pieces = state.pieces.all.filter(piece =>
piece.RoomId === room.Id
);
return {
...room,
Pieces: pieces
};
}
This isn't so efficient though since it will return a new object every time, causing the component to rerender more frequently than necessary. Depending on your scenario this may not be an issue. If it is, you should look into reselect, which will cache the result of GetRoomByName.
Also, the reason your code works when navigating from the home page is that you are returning a different reference from GetRoomByName because the roomName changes.
Related
I have a MobX store where I have a function doing an API call. It works fine it's getting the data but it doesn't update the already rendered page. I'm following this tutorial https://medium.com/#borisdedejski/next-js-mobx-and-typescript-boilerplate-for-beginners-9e28ac190f7d
My store looks like this
const isServer = typeof window === "undefined";
enableStaticRendering(isServer);
interface SerializedStore {
PageTitle: string;
content: string;
isOpen: boolean;
companiesDto: CompanyDto[],
companyCats: string[]
};
export class AwardStore {
PageTitle: string = 'Client Experience Awards';
companiesDto : CompanyDto[] = [];
companyCats: string[] = [];
loadingInitial: boolean = true
constructor() {
makeAutoObservable(this)
}
hydrate(serializedStore: SerializedStore) {
this.PageTitle = serializedStore.PageTitle != null ? serializedStore.PageTitle : "Client Experience Awards";
this.companyCats = serializedStore.companyCats != null ? serializedStore.companyCats : [];
this.companiesDto = serializedStore.companiesDto != null ? serializedStore.companiesDto : [];
}
changeTitle = (newTitle: string) => {
this.PageTitle = newTitle;
}
loadCompanies = async () => {
this.setLoadingInitial(true);
axios.get<CompanyDto[]>('MyAPICall')
.then((response) => {
runInAction(() => {
this.companiesDto = response.data.sort((a, b) => a.name.localeCompare(b.name));
response.data.map((company : CompanyDto) => {
if (company.categories !== null ) {
company.categories?.forEach(cat => {
this.addNewCateogry(cat)
})
}
})
console.log(this.companyCats);
this.setLoadingInitial(false);
})
})
.catch(errors => {
this.setLoadingInitial(false);
console.log('There was an error getting the data: ' + errors);
})
}
addNewCateogry = (cat : string) => {
this.companyCats.push(cat);
}
setLoadingInitial = (state: boolean) => {
this.loadingInitial = state;
}
}
export async function fetchInitialStoreState() {
// You can do anything to fetch initial store state
return {};
}
I'm trying to call the loadcompanies from the _app.js file. It calls it and I can see in the console.log the companies etc but the state doesn't update and I don't get to see the actual result. Here's the _app.js
class MyApp extends App {
constructor(props) {
super(props);
// Don't call this.setState() here!
this.state = {
awardStore: new AwardStore()
};
this.state.awardStore.loadCompanies();
}
// Fetching serialized(JSON) store state
static async getInitialProps(appContext) {
const appProps = await App.getInitialProps(appContext);
const initialStoreState = await fetchInitialStoreState();
return {
...appProps,
initialStoreState
};
}
// Hydrate serialized state to store
static getDerivedStateFromProps(props, state) {
state.awardStore.hydrate(props.initialStoreState);
return state;
}
render() {
const { Component, pageProps } = this.props;
return (
<Provider awardStore={this.state.awardStore}>
<Component {...pageProps} />
</Provider>
);
}
}
export default MyApp;
In the console.log I can see that this.companyCat is update but nothing is changed in the browser. Any ideas how I can do this? Thank you!
When you do SSR you can't load data through the constructor of the store because:
It's does not handle async stuff, so you can't really wait until the data is loaded
Store is created both on the server side and on the client too, so if theoretically constructor could work with async then it still would not make sense to do it here because it would load data twice, and with SSR you generally want to avoid this kind of situations, you want to load data once and reuse data, that was fetched on the server, on the client.
With Next.js the flow is quite simple:
On the server you load all the data that is needed, in your case it's loaded on the App level, but maybe in the future you might want to have loader for each page to load data more granularly. Overall it does not change the flow though
Once the data is loaded (through getInitialProps method or any other Next.js data fetching methods), you hydrate your stores and render the application on the server side and send html to the client, that's SSR
On the client the app is initialized again, though this time you don't want to load the data, but use the data which server already fetched and used. This data is provided through props to your page component (or in this case App component). So you grab the data and just hydrate the store (in this case it's done with getDerivedStateFromProps).
Based on that, everything you want to fetch should happen inside getInitialProps. And you already have fetchInitialStoreState method for that, so all you need to do is remove data fetching from store constructor and move it to fetchInitialStoreState and only return the data from it. This data will then go to the hydrate method of your store.
I've made a quick reproduction of your code here:
The huge downside if App.getInitialProps is that it runs on every page navigation, which is probably not what you want to do. I've added console.log("api call") and you can see in the console that it is logged every time you navigate to any other page, so the api will be called every time too, but you already have the data so it's kinda useless. So I recommend in the future to use more granular way of loading data, for example with Next.js getServerSideProps function instead (docs).
But the general flow won't change much anyway!
Calling awardStore.loadCompanies in the constructor of MyApp is problematic because the loadCompanies method is populating the store class. What you want is to hydrate the store with the companyCats data. Since server and client stores are distinct, you want to load the data you need on the server side i.e. fetchInitialStoreState (or load it from a page's getStaticProps/getServerSideProps method) so that you can pass it into the hydrate store method from page/app props.
Note loadCompanies is async so it'll be [] when getDerivedStateFromProps is called so there's nothing to hydrate. For your existing hydrate method to work you need initialStoreState to be something like the fetchInitialStoreState method below. Alternatively if it's fetched on the page level, the hydrate may be closer to initialData?.pageProps?.companyCats
It's common to see the store hydration as needed for each page though it's still valid to call loadCompanies() from the client side. There's a lot I didn't get a chance to touch on but hopefully this was somewhat helpful.
export const fetchInitialStoreState = async() => {
let companyCats = [];
try {
const response = await axios.get < CompanyDto[] > ('MyAPICall')
response.data.map((company: CompanyDto) => {
if (Array.isArray(company.categories) && company.categories.length > 0) {
companyCats.push(...company.categories)
}
})
} catch (error) {
// Uh oh...
}
return {
serializedStore: {
companyCats,
// PageTitle/etc
}
}
}
My code is not long or complicated at all. It's simple. so please read!
(Im using react + next.js)
In the root file, app.js, I have useEffect to fetch photo data. This data array will be used in a page component so I pass it down from app.js via <Component.../>
function MyApp({ Component, pageProps }) {
const [photoData, setPhotoData] = useState([]);
const [user, setUser] = useState([]);
useEffect(() => {
const getPhotos = async () => {
try {
const photoData = await axios.get(
"https://jsonplaceholder.typicode.com/albums"
);
setPhotoData(photoData.data);
} catch (error) {
console.log(error);
}
};
getPhotos();
}, []);
useEffect(() => {
//code for finding user. no external api used.
setUser(user);
}
}
}, []);
const passedProps = {
...pageProps,
photoData,
user
};
return (
...
<Component {...passedProps} />
)
Then I pass the data (photodata) from a Home component to a (app.js 's) grandchild component, an Photo component
export default function Home({ photoData, user }) {
return(
<Photo photoData={photoData} user={user} />
)
In Photo component, I am receiving photoData and trying to set a state for photoArr with the default state of photoData.
When the entire app is first loaded, the photoData is passed down to the Photo component successfully that it sets the state without any issue.
But the main problem is that when I am in the Photo page (photos are loaded) and refresh the page, then it does not set the state for photoArr with photoData. Even though I can console log photoData received from app.js, it does not set state, photoArr, with the default state, photoData.
export default function Photo({ photoData, user }) {
const [photoArr, setPhotoArr] = useState(photoData);
//I have this as state because I change this array
//later on in this component (doing CRUD on the photo array).
console.log(photoData); // this returns the array received from app.js
console.log(photoArr); // []. returns an empty array
console.log(user); // returns the user object received from app.js.
return (
<div>
{photoArr.length > 0 ?
.... code mapping photoArr
: "Data is empty" //It returns this comment when I refresh the page
}
</div>
)
As you can see above, when I refresh the page, I get "Data is empty" meaning photoArr was not set even with the given default state. If I keep refreshing the page multiple times, it still shows a blank page.
From my research, it's due to setting state being asynchronous? So then how can I fix this problem?
Try this:
(In your Photo page)
const [photoArr, setPhotoArr] = useState(null);
useEffect(() => {
if(photoData.length) setPhotoArr(photoData) // If not empty, set the Arr
},[photoData]} // We listen to photoData's change
On page load, there aren't any data in your photoData, and as it pass down to Photo component, react remembers that state.
But with useEffect listen to photoData's change, we can setPhotoArr once the getPhotos function got the data back.
I am working on React app where the state is managed by redux. I am using actions.js file to fetch JSON data and store it directly in the store. The initial Store has just one key (data) in its obj with null as its value.
I use componentDidMount() Lifecycle to call the function which updates the store's data key with the JSON data I receive. However, whenever I load my app it gives an error because it finds the data value as null.
I get it. componentDidMount() executes after the app is loaded and the error doesn't let it execute. I tried using componentWillMount() but it also gives the same error. ( Which I use in JSX )
When I try to chanage the data's value from null to an empty obj it works for some level but after I use it's nested objects and arrays. I get error.
I wanna know what is the way around it. What should I set the vaue of inital State or should you use anyother lifecycle.
If your primary App component can't function properly unless the state has been loaded then I suggest moving the initialization logic up a level such that you only render your current component after the redux state has already been populated.
class version
class LoaderComponent extends React.Component {
componentDidMount() {
if ( ! this.props.isLoaded ) {
this.props.loadState();
}
}
render() {
if ( this.props.isLoaded ) {
return <YourCurrentComponent />;
} else {
return <Loading/>
}
}
}
export default connect(
state => ({
isLoaded: state.data === null,
}),
{loadState}
)(LoaderComponent);
Try something like this. The mapStateToProps subscribes to the store to see when the state is loaded and provides that info as an isLoaded prop. The loadState in mapDispatchToProps is whatever action creator your current componentDidMount is calling.
hooks version
export const LoaderComponent = () => {
const dispatch = useDispatch();
const isLoaded = useSelector(state => state.data === null);
useEffect(() => {
if (!isLoaded) {
dispatch(loadState());
}
}, [dispatch, isLoaded]);
if (isLoaded) {
return <YourCurrentComponent />;
} else {
return <Loading />
}
}
And of course you would remove the fetching actions from the componentDidMount of the current component.
In my react app I have component named profile, and I am fetching data from server and showing it inside that component. I am using redux and redux-thunk along with axios. With help of mapDispatchToProps function, i am calling redux action for fetching that data when component is mounted and saving it to redux state. After that, using mapStateToProps function i am showing that data on the screen via props. That works fine. Now I want to have possibility to edit, for example, first name of that user. To accomplish that i need to save that data to component state when data is fetched from server, and then when text field is changed, component state also needs to be changed. Don't know how to save data to component sate, immediately after it is fetched.
Simplified code:
const mapStateToProps = (state) => {
return {
user: state.user
}
}
const mapDispatchToProps = (dispatch) => {
return {
getUserData: () => dispatch(userActions.getUserData())
}
}
class Profile extends Component {
state:{
user: {}
}
componentDidMount (){
this.props.getUserData()
// when data is saved to redux state i need to save it to component state
}
editTextField = () => {
this.setState({
[e.target.id]: e.target.value
})
};
render(){
const { user } = this.props;
return(
<TextField id="firstName"
value={user.firstName}
onChange={this.editTextField}
/>
)
}
}
You can use componentDidUpdate for that or give a callback function to your action.
I will show both.
First lets see componentDidUpdate,
Here you can compare your previous data and your present data, and if there is some change, you can set your state, for example if you data is an array.
state = {
data: []
}
then inside your componentDidUpdate
componentDidUpdate(prevProps, prevState) {
if(prevProps.data.length !== this.props.data.length) {
// update your state, in your case you just need userData, so you
// can compare something like name or something else, but still
// for better equality check, you can use lodash, it will also check for objects,
this.setState({ data: this.props.data});
}
}
_.isEqual(a, b); // returns false if different
This was one solution, another solution is to pass a call back funtion to your action,
lets say you call this.props.getData()
you can do something like this
this.props.getData((data) => {
this.setState({ data });
})
here you pass your data from redux action to your state.
your redux action would be something like this.
export const getData = (done) => async dispatch => {
const data = await getSomeData(); // or api call
// when you dispatch your action, also call your done
done(data);
}
If you are using React 16.0+, you can use the static method getDerivedStateFromProps. You can read about it react docs.
Using your example:
class Profile extends Component {
// other methods here ...
static getDerivedStateFromProps(props) {
return {
user: props.user
}
}
// other methods here...
}
I'm using Redux and React to load data from a web service which is working well. I'd like to make small non-webservice-based changes in the UI in response to an action. A simplified example:
class SmartComponent extends React.Component {
handleClick = (e) => {
// how to best handle a simple state change here?
}
render() {
const { displayMessage } = this.props
return (
<DumbComponent message={displayMessage}/>
<button onclick={this.handleClick}>Change Message</button>)
}
}
const mapStateToProps = state => {
// state variables linked in the reducer
}
export default connect(mapStateToProps)(SmartComponent)
let DumbComponent = ({ message }) => {
return ({message})
}
If I modify the state in SmartComponent, for instance, by using this.setState, the props of SmartComponent will not be automatically updated. I believe it's a React anti-pattern to directly modify the props of SmartComponent. Is the best way to update the message in DumbComponent to make an action creator and link it in the reducer? That seems a bit overkill for a simple message change.
Yes, you should link it to the reducer.
However this is not mandatory:
How to do it
One other way to do this would be to store the message in the state of the SmartComponent.
Beware about the fact that Redux is no longer the single source of truth for the message.
class SmartComponent extends React.Component {
constructor(props) {
super(props)
// Initialize state based on props
this.state = {
message: props.message,
}
}
componentWillReceiveProps(nextProps) {
// Handle state update on props (ie. store) update
this.setState({ message: ... })
}
handleClick = (e) => {
this.setState({ message: ... })
}
render() {
const { displayMessage } = this.state
return (
<DumbComponent message={displayMessage}/>
<button onclick={this.handleClick}>Change Message</button>)
}
}
const mapStateToProps = state => {
// state variables linked in the reducer
}
export default connect(mapStateToProps)(SmartComponent)
let DumbComponent = ({ message }) => {
return ({message})
}
Should you do it ?
If the data you display in this component can be completely isolated from the rest of your application, that is to say no dispatched action could modify it, and no other component need it, keeping this data in the store can be unnecessary.
I mostly use this method to perform optimistic updates to the view component without altering the store until new value is saved by the server.