React-redux: Trouble triggering child component action on parent state change - react-redux

I am passing a galleryReload prop to DisplayImages from UploadImages to re fetch a list of files after a new file is uploaded.
I am having trouble getting DisplayImages to run it's getImages action on the state change of the parent without causing an endless loop.
class UploadImage extends React.Component {
constructor(props) {
super(props);
this.state = {
galleryReload: false
};
...
// in render
<DisplayImages galleryReload={this.state.galleryReload} />
In DisplayImages I can get first display of images with:
//works fine on first load
componentWillMount() {
this.props.getImages(this.props.gallery);
}
}
I am really having trouble figuring out how to run the action this.props.getImages(this.props.gallery) again after a file has been uploaded.
I tried setting the state of {galleryReload: true} once upload was finished (in parent) like so:
//onUploadFinished
this.setState((prevState, props) => ({
galleryReload: true
}));
and fetching the results again in child with componentWillUpdate and other lifecycle methods, but I kept getting myself into an endless loop of fetching.
Here is an example of what I put into the various lifecycle methods to fetch and then stop further fetches until the next upload completes:
if (this.props.galleryReload) {
this.props.getImages(this.props.gallery);
this.setState((prevState, props) => ({
galleryReload: false
}));
}
How do I rework this to call getImages just one time after image upload?

I realized that in learning react and redux at the same time I was confusing state. I was not setting redux state in parent, but rather local state. So to reset the local state of parent component from child component I needed to use a callback function in parent that sets the state of galleryReload and pass it as prop to child component.
In my parent I added the function:
resetGalleryReload() {
this.setState({
galleryReload: false
});
}
I also set in parent constructor
this.resetGalleryReload = this.resetGalleryReload.bind(this);
Then I sent to child component as prop like so
<DisplayImages
galleryReload={this.state.galleryReload}
resetReloadGallery={this.resetGalleryReload}
/>
Finally, I educated myself on the use of lifecycle methods with this nice read:
https://developmentarc.gitbooks.io/react-indepth/content/life_cycle/introduction.html
So I created this in DisplayImages component
componentDidUpdate(prevProps, prevState) {
let val = this.props.galleryReload;
if (val) {
this.props.getImages(this.props.gallery);
this.props.resetReloadGallery();
}
}
I

Related

Svelte - on:event listener removal

I'm wondering whether assigning undefined to an element's event, i.e. on:mousemove prevents a memory leak the same as removeEventListener does, or at least should not be a concern long term. I've checked with getEventListeners on chrome, and it's there with the on:mousemove approach, but I'm not sure whether I should worry about this and use the more verobse approach with custom actions.
I have a Dropdown inside a Container. On moving the mouse over the container, I want to change the Dropdown's position.
My initial approach was writing a custom use:containerMouseMove action, that took Dropdown's visible as a dependency, and removed the event listener from the container, when the Dropdown became invisible.
Dropdown.svelte:
use:mousemoveContainer={{ container, destroyOn: !visible }}
on:mousemove_container={(e) => {
if (mouseTrack) {
[x, y] = calcCoordinates(e, container);
}
}}
Action definition:
type Deps = { container: HTMLElement; destroyOn: boolean };
export const mousemoveContainer = (node: HTMLDivElement, deps: Deps) => {
const handleMouseMove = (e: MouseEvent) => {
node.dispatchEvent(new CustomEvent('mousemove_container', { detail: e }));
};
return {
update(deps: Deps) {
if (!deps.destroyOn) {
deps.container.addEventListener('mousemove', handleMouseMove);
}
if (deps.destroyOn) {
deps.container.removeEventListener('mousemove', handleMouseMove);
}
}
};
};
Then I learned about export const function as a way to communicate between parent and child, and it simplifies the code. But I'm not sure if there's not a memory leak right now.
on:mousemove={dropdown.getVisible() ? dropdown.onContainerMouseMove : undefined}
onContainerMouseMoveis the callback inside on:mousemove_container.
on:event listeners are removed automatically when the component is unmounted.
Within actions, one should return an object with a destroy() function and unsubscribe from events there. The function will be called when the element with the action on it is removed.

adding logic to redux reducer to persist state

I am working with a react/redux learning project, where I'm building components to host headless CMS content. One part of the application is a dropdown that will select the content from all available content channels in the source CMS.
This works on the first pass, but when I navigate to another page (ie, the detail of a single CMS content item - the first page displays multiple items in a grid) it resets the state back to an initial (empty) variable.
The component is below:
import { FETCH_CHANNELS } from '../actions/types';
// set the initial state first
const initialState = {
isLoading: true,
data: [],
error: ""
}
// set the state depending on the dispatch coming in
const channelsReducer = (state = initialState, action) => {
switch(action.type) {
case FETCH_CHANNELS:
// reduce the returned state down to an array of options
// filter here to limit to searchable only
const activeChannels = [];
action.payload['channels'].forEach( el => {
if(el.isChannelSearchable) {
let singleItem = {
key: el.channelId,
value: el.channelId,
text: el.channelName,
type: el.channelType
}
activeChannels.push(singleItem);
}
});
return {...state, data: activeChannels, isLoading: false};
case "ERROR":
return {...state, error: action.msg};
default:
return state;
}
}
export default channelsReducer;
My issue here (as I see it), is the initialisation of the initialState constant at the beginning, everytime that the component is refreshed, it is set to empty again. Makes sense.
How can I persist the state that is returned in the FETCH_CHANNELS case (that action is a call to a back end api that returns all channels) so that upon the component remounting it still retains it's state?
Not sure if I have to either (quite possibly none of these are correct):
Attempt with some logic in the front end component that is calling this action, to not call it if data already exists?
Create another piece of state in the redux store and update that state from the front end component once a value from the drop down has been selected?
or 3. Try and handle it here with setting a variable in the reducer and logic to return that if necessary?
Like I said, I'm building this to try and learn a bit about react and redux, but i'm really not sure what the way to handle this is...
update, as suspected... neither of those options were correct. I was not calling the link correctly in the component generating the click event to drill into the detail content item. Implementing Link from react-router-dom was the right way to handle this.

Behavior Subject RXJS

I have this link that changes the final of the url with a time stamp:
getAvatar(channelId): BehaviorSubject<string> {
return new BehaviorSubject(`${link}?${new Date().getTime()}`);
}
And in the Avatar Component, that is a child of at least 10 other components i call this subscribe:
getAvatar() {
this.userService.getAvatar(this.channelId)
.subscribe(res => {
this.avatar = res;
this.cdr.detectChanges();
this.cdr.markForCheck();
});
}
OBS: Im using the OnPush changeDetection strategy
And in another component i have a function that changes this profile picture inside the link:
this.userService.changeProfilePicture(picture, this.myChannelId)
.subscribe(
() => {
this.loading.hide();
this.userService.getAvatar(this.id);
this.screenService.showToastMessage('Foto de perfil alterada.', true);
}
As you can see, im recalling the getAvatar() function that returns a BehaviorSubject to generate another link and the AvatarComponent doenst detect the change of the behaviorSubject, what im doing wrong ?
And theres another way to recall the getAvatar() function of all the AvatarComponent instances to reload each avatar instance ?
OBS2: I tried to use of rxjs operator, creating a new Observable(), tried the Subject class, all of those seems to not get detected by the subscribe inside the AvatarComponent
OBS3: I tried to get the AvatarComponent with #ViewChild() and call this this.avatarCmp.getAvatar(); to reload the avatar, but reloads just one instance of the Avatar Component
your service needs to be more like this:
private avatarSource = new BehaviorSubject(`${link}?${new Date().getTime()}`);
avatar$ = this.avatarSource.asObservable();
setAvatar(channelId): BehaviorSubject<string> {
this.avatarSource.next(`${link}?${new Date().getTime()}`);
}
then you need to subscribe to avatar$ and update with setAvatar

How to handle long async operations in Next.js to avoid slow page loading?

When using next-redux-wrapper how do I start a long asynchronous task so that it only runs on the client? I don’t want to use await on the server side since it would delay the initial page load. I’d rather set a loading flag as the task starts and show a loading indicator until it completes.
Let’s say my async operation looks like this:
function async takesLong(store) {
store.dispatch({type: “LOADING”, payload: true});
const result = await longOperation();
store.dispatch({type: “SETDATA”}, payload: data);
return store.dispatch({type: “LOADING”, payload: false});
}
I can call this in my Next page’s getInitialProps function like this:
MyPage.getInitialProps = async ({ store, isServer }) => {
const loader = takesLong(store)
if (isServer) await loader; // <-- will delay client response
return {
someprop: "some value"
};
};
This works well if the page loads on the client side. The operation starts, and my page can display a loading-spinner until the operation completes. But when started on the server side I have a long delay before the page displays at all. I’ve tried a number of approaches but can’t find one that works properly:
Starting the process on the server and not using await renders the page without the results being written to the store, so it has only set “loading” to true in the store and it never gets the data.
Passing store in my props doesn’t work - it ends up being an empty object ({ }) in the client.
Trying to run it inside my component doesn’t seem to work for a few reasons:
a) I don’t have the store object accessible in the React Component (only in getInitialProps which won’t get called on the client).
b) Even if I use actions instead of store.dispatch in the Component, when can I call it safely? I can’t do it during render since it will change the Redux state, and componentWillReceiveProps won’t get called before the first client-side render
Is there a well defined pattern for deferring a long operation to the client-side when using Next?
Do your long async task on componentDidMount, it will run only on client side.
React in SSR not runs componentDidMount lifecycle hook.
Using bound actions during componentDidMount works. Thanks to #eenagy for the suggestion. Doing things in this order seems to do what is needed:
import { bindActionCreators } from "redux";
import { setLoading, setError, setData } from "../actions";
componentDidMount() {
if (!this.props.haveData && !this.props.loading && !this.props.error) {
this.props.setLoading(true);
loadSomeData() // <-- this takes a while to complete
.then( data => {
this.props.setData(data);
this.props.setLoading(false);
})
.catch( err => {
this.props.setError(err);
this.props.setLoading(false);
});
}
}
render() {
if (this.props.loading) return (<Loading/>);
return (/*regular page*/);
}
export const mapDispatchToProps = dispatch => {
return bindActionCreators({ setLoading, setError, setData }, dispatch);
};
export default connect(mapStateToProps, mapDispatchToProps)(Component);
This way if the initial data is not already loaded (say by another page) it will get kicked off when the
component mounts and run asynchronously until the operation completes and
calls the actions in redux to cause the page to re-render.

React Redux Action Creator Dispatch Issue with Fetch API and Promise

I am writing action creator in react app. where in when i do some api call i need to show the Progress Loader on screen. So, my action creator looks like this.
export const fetchData = (actionType, param) => (dispatch) => {
dispatch(Action(ActionConstants.SHOW_PROGRESS_LOADER)); // Show Loader Action
return fetchDataRequest(actionType, param) // Here is Fetch APi Call
.then(responseData => {
dispatch(Action(ActionConstants.HIDE_PROGRESS_LOADER));
dispatch(Action(recd(actionType), { data: responseData, receivedAt: Date.now() }));
}).catch((error) => {
dispatch(Action(ActionConstants.HIDE_PROGRESS_LOADER)); // Hide Loader Action
});
};
When i write this piece of code its working as expected, i am dispatching the action as dispatch(fetchData(data)) from component and i am able to show the loader in my Parent Component. What i understand is fetch is returning me the promise. Once the fetch gets completed then i am hiding the loader which is working as expected.
Now, There is scenario where in i need to do some validation where in i don't have to make any api call but all the validation are performed locally.
Here also i want to do the same thing like i need to show loader in my parent component as well when all the validation are done i need to hide the loader.
I have written the same piece of code even actions are getting called but my render function is not getting called.
My Code Looks like:
// This my action creator which will actually do the validation
export const validateAndSaveData = () => {
return ((dispatch, getState) => {
return new Promise((resolve, reject) => {
let saveRecommendDetailsFlag = true;
// here i am dispacthing some action and storing data in my store
saveRecommendDetailsFlag = canSaveData(getState());
if (saveRecommendDetailsFlag) {
resolve('SUCCESS');
} else {
reject('ERROR');
}
});
});
};
And there is one more action creator which i am calling it from from UI Component which will first initiate the show loader action and then perform validation and based on the result of validation i have to hide the loader.
export const saveData = () => {
return ((dispatch) => {
dispatch(Action(ActionConstants.SHOW_PROGRESS_LOADER)); // Show Loader Action
return dispatch(validateAndSaveData())
.then(() => {
// Here i m dispatching an action to do some more processing.
dispatch(Action(ActionConstants.HIDE_PROGRESS_LOADER)); // Hide Loader Action
})
.catch(() => {
dispatch(Action(ActionConstants.HIDE_PROGRESS_LOADER)); // Hide Loader Action
});
});
};
Everything is working fine but my loader are not coming on the screen. i am not able to figure it out where am i doing wrong.
Can anyone suggest something how can i solve this issue?
I got some workaround using setTimeout func but i don't think that is right approach.
export const saveData = () => {
return ((dispatch) => {
dispatch(Action(ActionConstants.SHOW_PROGRESS_LOADER)); // Show Loader Action
setTimeout(()=>return dispatch(validateAndSaveData())
.then(() => {
// Here i m dispatching an action to do some more processing.
dispatch(Action(ActionConstants.HIDE_PROGRESS_LOADER)); // Hide Loader Action
})
.catch(() => {
dispatch(Action(ActionConstants.HIDE_PROGRESS_LOADER)); // Hide Loader Action
});
},10);
});
};
Your code looks reasonable, my suspicion is that your validateAndSaveData promise finishes so quickly that there is no visible loader on the screen.
In that case, a timeout is totally reasonable. However, in order to do it properly, I would keep a state on if the loading screen is visible + if it's been shown long enough. You can then remove the loading screen once it is both up for long enough, and the actual event expires.
I'm not sure which action package you're using, so I can't post exact code, but the pseudocode would look something like this:
const delay = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds));
let loadingCounter = 0;
const showLoadingScreen = () => (dispatch) => {
const counter = loadingCounter;
loadingCounter++;
delay(5).then(() => {
if (getStore().loadingScreen.counter === counter) {
dispatch(Action(ActionConstants.PROGRESS_LOADER_DELAY_ELAPSED))
}
})
return dispatch(Action(ActionConstants.SHOW_PROGRESS_LOADER, counter))
}
Basically, you would keep track of 3 pieces of state for the loader:
{
counter: 0,
taskCompleted: false,
canHide: false,
}
Counter is saved so that you can disambiguate what happens if you get SHOW_PROGRESS_LOADER while an existing SHOW_PROGRESS_LOADER is in progress.
taskCompleted keeps a record of whether the thing you're waiting on is done, and canHide keeps track if the loader has been visible on the screen long enough.
When you dispatch PROGRESS_LOADER_DELAY_ELAPSED it sets canHide to true, and when you dispatch HIDE_PROGRESS_LOADER it sets taskCompleted to true. (although you may want to rename the latter action). When both canHide and taskCompleted are set to true, only then can the loader go away.
This is a pretty common UI pattern - Try to complete a task quickly. If it takes more than a short amount of time, then throw up a loading dialog. However, the loading dialog is guaranteed to stay up a minimum amount of time to prevent flickering. So the more advanced version of this kind of pattern would be to add another state which doesn't show the progress loader at all unless the call takes more than Y milliseconds.
Hope this makes sense, leave a comment if not :-)

Resources