I'm working on integrating Redux in an already finished SPA with ReactJS.
On my HomePage I have a list of the 4 newest collections added which on render, I fetch with axios from my database. These are then saved in Redux Store and displayed on the React UI.
My mapStateToProps look something like this:
const mapStateToProps = (state) => ({
credentials: credentials(state),
collections: collections(state)
});
Where credentials is irrelevant and collections is:
const collections = (state) => {
if (state.collectionsHomeViewReducer.fetching === true) {
return {
fetchingCollections: true
}
}
else if (state.collectionsHomeViewReducer.data) {
const response = state.collectionsHomeViewReducer.data;
return {
collections: response.collections,
fetchedCollections: true,
fetchingCollections: false
}
}
else if (state.collectionsHomeViewReducer.fetched === false) {
return {
fetchedCollections: false,
fetchingCollections: false
}
}
};
What is it I want to do:
Update the store state every time another client, or the current client, adds a new collection. Moreover, I do not wish for the UI to update immediately after I dispatch(action), I want it to update when a user refreshes the page or when he navigates to another view and returns ( I believe what I'm trying to say is when componentDidMount is called ).
What have I achieved so far:
By using socket.io, I
socket.emit("updateCollectionsStore")
socket.on("updateCollectionsStore")
and
socket.broadcast.emit("updateCollectionsStore")
in their respective places in the application. The final call of
socket.on("updateCollectionsStore")
after the broadcast, is in the main file of the page, app.jsx where the store is also located. The function there looks like this:
socket.on("updateCollectionsStore", () => {
store.dispatch(getCollectionsHomeView());
});
The store is updated and everything works fine, as viewed from the Redux Dev Tools.
What I can't seem to figure out is to tell the props not to change due to the fact that mapStateToProps is called every time an action is dispatched.
Why do I need this: The HomePage can deal with a continuous UI update and data fetching but I also have a page ReadAllPage where you can real all collections. The problem is if there will always be the newest post on the top, whenever a new one is added, the current one is pushed downwards. In case somebody had the intent to click the one that was pushed down, now he might have accidentally clicked the one that took its place, which is not wanted.
What else should I do different or further to achieve the result I want?
Thank you.
According to your needs, I would have two properties in the state. First is that is currently visible on the HomeView and the second is that is updated via sockets. Once a user navigates to the HomeView you can just replace the first collection with the second one.
Related
Is it bad practice to use useEffect?
And should useEffect be avoided if possible due to re-renders?
This question arised yesterday when a colleague asked for a code review and we had different opinions on how to solve this.
We are creating an app that shows some kind of documentation which could be sorted in chronological or reversed chronological order. This is decided by a button in the apps top bar with a default value of chronological order, this value is stored in a global redux state and will be used in every call to fetch documentation.
In this example we update sortOrder on button click and as an effect of that we fetch data.
If I understand this correctly, we render once when sortOrder state change, and once after data is fetched.
Pseudo code ish
interface AppState = {
sortOrder: SortOrder:
documentation: Documentation[];
}
reducer(){
case toggleSortOrder:
const order = state.sortOrder === 'asc' ? 'desc' : 'asc';
return {
....state,
sortOrder: order;
}
}
const AppBar = () => {
const dispatch = useDispatch();
return <div><button onClick={dispatch(toggleSortOrder)}>Change sort order</button>
</div>;
}
const DocumentationList = (type: DocumentationType) => {
const dispatch = useDispatch();
const sortOrder = useSelector((state) => state.appState.sortOrder);
const documentation = useSelector((state) => state.appState.documentation);
useEffect(() => {
// action is caught by redux-saga and a call to docApi is made through axios
dispatch(getDocumentation.request(type, sortOrder)
},[sortOrder]);
return documentation.map((doc) => <Documentation data={doc} />);
}
Is this bad practice?
Should we avoid useEffect and fetch data on click and update sortOrder in saga instead?
Reading docs and blogs I mostly see examples of how en when to use them.
In my opinion, I would go with solution more-less like yours, with splitting responsibilities between element which externally changes query params, and element which is displaying data based on current query params. If you decide to put all logic in button click handler then you are kinda coupling too much list and button, because in order to delegate all work to button click you must dig into DocumentationList fetch-data implementation and to copy it to another place(button related saga or in button click handler) in order to fetch data from another place in the app, and not just from the DocumentationList itslef.
From my perspective, only DocumentationList should be responsible to fetch data, and noone else. But you should provide way to subscribe, from documentation list, to some external query params(sort, filters etc) and when they change(if they exist) data should be loaded.
Right now you only have sort, but in case when you can potentially have more params that can be externally modified, then I would dedicate more complex redux part to query params, something like documentationQueryParams: { sort: "asc", filters: { name: "doc a", type: "type b" } }, and then inside DocumentationList I would use custom hook, for example const queryParms = useDocumentationQueryParams(); which will return standardized query params, and in useEffect I would subscribe to those queryParms change - whenever they change I will easily fetch new data since you know what is the structure of the queryParms(they must be standardized is some way). Like this you coupled them but in very flexible way, whenever you need new param you will update only filter/query-related component, because in DocumentationList you relay on standardized hook output and you can easily create generic mechanism to output query string, or body data, in order to make new request and to fetch new data.
In terms of performance, there is really no difference between hooks-based approach and moving all to click handlers, because in DocumentationList your render part should rerender only when list change, no matter how list is being changed.
It must be pretty regular issue.
I'm passing props down to the children and I'm using it there to request to the endpoint. More detailed: I'm clicking on the list item, I'm checking which item was clicked, I'm passing it to the child component and there basing on prop I passed I'd like to request certain data. All works fine and I'm getting what I need, but only for the first time, ie. when refreshing page incoming props are gone and I cannot construct proper URL where as a query I'd like to use the prop value. Is there a way to preserve the prop so when the page will be refresh it will preserve last prop.
Thank you!
(You might want to take a look at: https://github.com/rt2zz/redux-persist, it is one of my favorites)
Just like a normal web application if the user reloads the page you're going to have your code reloaded. The solution is you need to store the critical data somewhere other than the React state if you want it to survive.
Here's a "template" in pseudo code. I just used a "LocalStorage" class that doesn't exist. You could pick whatever method you wanted.
class Persist extends React.Component {
constuctor(props) {
this.state = {
criticalData = null
}
}
componentDidMount() {
//pseudo code
let criticalData = LocalStorage.get('criticalData')
this.setState({
criticalData: criticalData
})
}
_handleCriticalUpdate(update) {
const merge = {
...LocalStorage.get('criticalData')
...update
}
LocalStorage.put('criticalData', merge)
this.setState({
criticalData: merge
})
}
render() {
<div>
...
<button
onClick={e => {
let update = ...my business logic
this._handleCriticalUpdate(update) //instead of set state
}}
>
....
</div>
}
}
By offloading your critical data to a cookie or the local storage you are injecting persistence into the lifecycle of the component. This means when a user refreshes the page you keep your state.
I hope that helps!
I know there are some similar topics but none seems to be in the same direction of what I'm trying to do, thus a new thread.
I have a component that displays a list of keys, each with a checkbox attached to the string. In addition, I have a button that supposedly calls an API with all keys selected and delete these keys.
Several things I'm trying to achieve:
checking a check box enables the delete button
click the delete button should send a POST to API, the list should then reload
Since the list is reloaded, all checkbox should be unselected, thus the delete button is once again disabled
there's another button outside of this function that checks for the length of the list as well, which I don't know how to associate with this list if I fetch the list in the component.
I'm facing the problem which I don't know how to make the button and the checkboxes associate to each other. I tried using state with a checked state, which is a boolean, but that's only one boolean and cannot record several keys. I think using an array would work? Then again I'm not sure how to properly append or remove the key checked.
my code looks like
class AppList extends Component {
constructor() {
super();
this.state = {
checked: [],
apps: []
};
this.handleChecked = this.handleChecked.bind(this);
}
componentDidMount() {
fetch("some_url", {
method: 'post',
body: JSON.stringify({"user": "some_email"}),
headers: {'Content-Type': ' application/json'}
})
.then(res => res.json())
.then(
(result) => {
this.setState({apps: JSON.parse(result)});
},
(error) => {
console.log("error", error);
}
);
}
handleDeleteKey = (event) => {
// fetch(I'll worry about this later)
this.setState({checked: false});
console.log("delete!!!!");
}
handleChecked () {
this.setState({checked: !this.state.checked});
}
render () {
const apps = this.state.apps.map((app) =>
<div>
<input type="checkbox" onChange={this.handleChecked} />
{` ${app}`}
</div>
);
return (
<div>
<h4>Client Key List:</h4>
{this.state.apps.length > 0 ? <ul>{apps}</ul> : <p>No Key</p>}
{this.state.apps.length > 0 ? <button className="api-instruction-button" onClick={this.handleDeleteKey}>Delete Selected Key(s)</button> : null}
</div>
);
}
}
export default AppList;
I feel like my design is completely wrong but I don't know how to fix it. It seems like there are so many states to be passed around and nothing is the outermost, almost a cyclic dependency.
Anyone had any experience dealing with this problem? It seems like it's a common user action but I can't figure it out.
EDIT: after digging it a bit more, it seems like I need to call componentDidMount outside of the AppList. It should be in the component that uses AppList, let's call it MainApp.
MainApp calls componentDidMount which is the same as the one in AppList. The one in AppList gets removed, and the keys are passed to AppList as props.
I have trouble handling the clicking event. It seems like the component is always updating, so if I want to append the clicked key to the array, it wouldn't work. The same call will be made again and again.
Since there's another button in MainApp that requires the list of keys, I can't just pass the call into AppList. However, updating in AppList should update the MainApp as well. How does it work? I'm so confused
EDIT2:
https://codesandbox.io/s/7w2w11477j
This recreation should contain all functions I have so far, but I can't get them to work together.
Again my task is simply:
I have a list of strings, each with a checkbox
checking the checkbox selects the specific string
There's a button that I can click to delete these entries in my db by calling an API
Is refreshing the MainApp needed in this case? Otherwise I need to delete the strings in frontend so they don't display after the delete button is pressed
Here's what I believe you were going for: https://codesandbox.io/s/w23wv002yw
The only problem that made yours not work properly was you were just getting a little jumbled with where to put everything.
Contents:
The MainApp.js will only contain the apps and a method for deleting them in the backend. Other than those two methods, nothing else really concerns the MainApp.js file.
The AppList.js will contain all the methods that update its own checked state, the delete button itself, and a method to clear the checked state on delete.
Processes:
First, MainApp.js will load and remount with a backend api pull and populate its apps state. Once it's finished that, it will pass it on to AppList.js. From there, AppList.js will render that list as a multi-select field onscreen. The user can then select or deselect any of the options. As an option is selected, its index is pushed to the checked state and organized in ascending order.
(ordering the array isn't that necessary, but I figured it would help if you wanted to retool it sometime down the road)
When one or more option is selected, a delete button will appear. When the user clicks the delete button, AppList.js will call the delete function passed to it from MainApp.js, then it will clear the current checked state.
The Set Up
I have a React/Redux application that loads a list of cats from an API.
The data gets loaded into a component like so:
// thunk, etc omitted for clarity.
componentDidMount() {
if(!this.props.loaded){
this.props.actions.loadRooms();
}
}
Which draws its props from here:
function mapStateToProps(state, ownProps) {
return {
cats: state.cats.items,
loaded: state.cats.loaded
}
}
Assume the following:
1) cats will be needed in a different, entirely separate component, one that is not a child of the current component.
2) I have no way of knowing which of the cats requiring components will be mounted first.
The Actual Question
Is the if(!this.props.loaded) useful? Put another way, does it save me a theoretical call to the API when that other route mounts if both check for existing store data first?
If the check is useful, is there a better way to do it?
Yes, I would have your redux actions look something like: GET_CATS, GET_CATS_SUCCESS, and GET_CATS_ERROR.
GET_CATS would set the loading state in the redux store to true, that way you can interrogate it in the respective componentDidMount() functions and only make the call to the api when loading is false. I think this is a fairly common way of doing it.
It all depends on how you handle your async data fetching in redux ,if both siblings components are listening to the portion of the state that represents cats you can do:
// Component A and Component B might have something like this
// they both subscribe to the same portion of the state so, if
// data is already available then you don't need to do fetch it again.
...
componentDidMount() {
if (this.props.cats.length === 0) {
this.props.actions.loadRooms();
}
}
...
If you are using redux-thunk then you might control this at the action level:
function loadRooms() {
return (dispatch, getState) => {
if (getState().cats.length === 0) {
dispatch(loadRoomsPending());
fetchMyData(...args)
.then((res) => dispatch(loadRoomsSuccess(res))
.catch((err) => dispatch(loadRoomsError(err));
}
}
}
// Component A and Component B
...
componentDidMount() {
this.props.actions.loadRooms();
}
...
Again here you have access to the current state with getState() so it's pretty common to check if the data is already available. Now this approach comes with some boilerplate and it might get tedious in the long run (it requires for you to write another three functions loadRoomsPending, loadRoomsSuccess, loadRoomsError). This way your components don't have to manually check for it. Or if you like it more explicit or cleaner you can give a middleware I implemented a try, I was kind of frustrated by all this boilerplate so using redux-slim-async you can do this:
function loadRooms() {
return {
types: [
actionTypes.LOAD_ROOMS_PENDING,
actionTypes.LOAD_ROOMS_SUCCESS,
actionTypes.LOAD_ROOMS_ERROR,
],
callAPI: fetch(...args).then(res => res.json()),
shouldCallAPI: (state) => state.cats.length === 0,
};
}
This handles everything for you with FSA compliant actions and it's very clear what is going on. Heck if you set it up properly you can make it even better:
function loadRooms() {
return {
typePrefix: actionTypes.LOAD_ROOMS,
callAPI: fetch(...args).then(res => res.json()),
shouldCallAPI: (state) => state.cats.length === 0,
};
}
And this will fire off the pending, success and error request with the format ${typePrefix}_PENDING, ${typePrefix}_SUCCESS, ${typePrefix}_ERROR, You can find the middleware here. But by all means just use whatever you feel best fits your use case, I felt like sharing this work because it's a frustration that brought me to build a middleware to handle it. Keep in mind that I made some assumptions on your case so if I am completely off let me know.
if I understand your question correctly, you want to be able to see if a separate class is loaded its data yet. If yes, then don't call the API to load the cats again.
There are two ways to do this, let's assumed COM1 and COM2 are your components.
return the entire state instead of just the specific variables you want for both of your components:
return state
then reference the cats in each component:
this.props.COM1.cats.items
this.props.COM2.cats.items
return the specific cats variable from the other components. you do the following for each components:
function mapStateToProps(state, ownProps) {
let cats = state.COM1.cats.items;
let loaded: state.cats.loaded;
let otherCats = state.COM2.cats.items;
return {
cats,
otherCats,
loaded
}
}
The Facebook Flux dispatcher explicitly prohibits ActionCreators from dispatching other ActionCreators. This restriciton is probably a good idea since it prevents your application from creating event chains.
This however becomes an issue as soon as you have Stores containing data from asynchronous ActionCreators that depend on each other. If CategoryProductsStore depends on CategoryStore there doesn't seem to be a way to avoid event chains when without resorting to deferring the follow-up action.
Scenario 1:
A store containing a list of products in a category needs to know from which category ID it should fetch products from.
var CategoryProductActions = {
get: function(categoryId) {
Dispatcher.handleViewAction({
type: ActionTypes.LOAD_CATEGORY_PRODUCTS,
categoryId: categoryId
})
ProductAPIUtils
.getByCategoryId(categoryId)
.then(CategoryProductActions.getComplete)
},
getComplete: function(products) {
Dispatcher.handleServerAction({
type: ActionTypes.LOAD_CATEGORY_PRODUCTS_COMPLETE,
products: products
})
}
}
CategoryStore.dispatchToken = Dispatcher.register(function(payload) {
var action = payload.action
switch (action.type) {
case ActionTypes.LOAD_CATEGORIES_COMPLETE:
var category = action.categories[0]
// Attempt to asynchronously fetch products in the given category, this causes an invariant to be thrown.
CategoryProductActions.get(category.id)
...
Scenario 2:
Another scenario is when a child component is mounted as the result of a Store change and its componentWillMount/componentWillReceiveProps attempts to fetch data via an asynchronous ActionCreator:
var Categories = React.createClass({
componentWillMount() {
CategoryStore.addChangeListener(this.onStoreChange)
},
onStoreChange: function() {
this.setState({
category: CategoryStore.getCurrent()
})
},
render: function() {
var category = this.state.category
if (category) {
var products = <CategoryProducts categoryId={category.id} />
}
return (
<div>
{products}
</div>
)
}
})
var CategoryProducts = React.createClass({
componentWillMount: function() {
if (!CategoryProductStore.contains(this.props.categoryId)) {
// Attempt to asynchronously fetch products in the given category, this causes an invariant to be thrown.
CategoryProductActions.get(this.props.categoryId)
}
}
})
Are there ways to avoid this without resorting to defer?
Whenever you are retrieving the state of the application, you want to be retrieving that state directly from the Stores, with getter methods. Actions are objects that inform Stores. You could think of them as being like a request for a change in state. They should not return any data. They are not a mechanism by which you should be retrieving the application state, but rather merely changing it.
So in scenario 1, getCurrent(category.id) is something that should be defined on a Store.
In scenario 2, it sounds like you are running into an issue with the initialization of the Store's data. I usually handle this by (ideally) getting the data into the stores before rendering the root component. I do this in a bootstrapping module. Alternatively, if this absolutely needs to be async, you can create everything to work with a blank slate, and then re-render after the Stores respond to an INITIAL_LOAD action.
For scenario 1:
I would dispatch new the action from the view itself, so a new action -> dispatcher -> store -> view cycle will trigger.
I can imagine that your view needs to retrieve the category list and also it has to show, by default, the list of products of the first category.
So that view will react to changes con CategoryStore first. Once the category list is loaded, trigger the new Action to get the products of the first category.
Now, this is the tricky part. If you do that in the change listener of the view, you will get an invariant exception, so here you have to wait for the payload of the first action to be completely processed.
One way to solve this is to use timeout on the change listener of the view. Something similar to what is explained here:
https://groups.google.com/forum/#!topic/reactjs/1xR9esXX1X4 but instead of dispatching the action from the store, you would do it from the view.
function getCategoryProducts(id) {
setTimeout(() => {
if (!AppDispatcher.isDispatching()) {
CategoryProductActions.get(id);
} else {
getCategoryProducts(id);
}
}, 3);
}
I know, it is horrible, but at least you won't have stores chaining actions or domain logic leaking to action creators. With this approach, the actions are "requested" from the views that actually need them.
The other option, which I haven't tried honestly, is to listen for the DOM event once the component with the list of categories is populated. In that moment, you dispatch the new action which will trigger a new "Flux" chain. I actually think this one is neater, but as said, I haven't tried yet.