So I'm creating what is at it's core a very simple CRUD-style application, using React + Redux. There is a collection of (lets call them) posts, with an API, and I want to be able to list those and then when the user clicks on one, go into a detail page about that post.
So I have a posts reducer. Originally I started using the approach taken from the redux real-world example. This maintains a cache of objects via an index reducer, and when you do a "get post" it checks the cache and if it's there, it returns that, else it makes the appropriate API call. When components mount they try to get things from this cache, and if they're not there they wait (return false) until they are.
Whilst this worked OK, for various reasons I now need to make this non-caching i.e. everytime I load the /posts/:postId page I need to get the post via the API.
I realise in the non-redux world you would just do a fetch() in the componentDidMount, and then setState() on that. But I want the posts stored in a reducer as other parts of the app may call actions that modify those posts (say for example a websocket or just a complex redux-connected component).
One approach I've seen people use is an "active" item in their reducer, like this example: https://github.com/rajaraodv/react-redux-blog/blob/master/public/src/reducers/reducer_posts.js
Whilst this is OK, it necessitates that each component that loads the active post must have a componentWillUnmount action to reset the active post (see resetMe: https://github.com/rajaraodv/react-redux-blog/blob/master/public/src/containers/PostDetailsContainer.js). If it did not reset the active post, it will be left hanging around for when the next post is displayed (it will probably flash for a short time whilst the API call is made, but it's still not nice). Generally forcing every page that wants to look at a post to do a resetMe() in a componentWillUnmount fells like a bad-smell.
So does anyone have any ideas or seen a good example of this? It seems such a simple case, I'm a bit surprised I can't find any material on it.
How to do it depends on your already existing reducers, but i'll just make a new one
reducers/post.js
import { GET_ALL_POSTS } from './../actions/posts';
export default (state = {
posts: []
}, action) => {
switch (action.type) {
case GET_ALL_POSTS:
return Object.assign({}, state, { posts: action.posts });
default:
return state;
}
};
It is very easy to understand, just fire an action to get all your posts and replace your previous posts with the new ones in the reducer.
Use componentDidMount to fire the GET_ALL_POSTS action, and use a boolean flag in the state to know if the posts where loaded or not, so you don't reload them every single time, only when the component mounts.
components/posts.jsx
import React from 'react';
export default class Posts extends React.Component {
constructor(props) {
super(props);
this.state = {
firstLoad: false
};
}
componendDidMount() {
if (!this.state.firstLoad) {
this.props.onGetAll();
this.setState({
firstLoad: true
});
}
}
// See how easy it is to refresh the lists of posts
refresh() {
this.props.onGetAll();
}
render () {
...
// Render your posts here
{ this.props.posts.map( ... ) }
...
}
}
We're just missing the container to pass the posts and the events to the component
containers/posts.js
import { connect } from 'react-redux';
import { getPosts } from './../actions/posts';
import Posts from './../components/posts.jsx';
export default connect(
state => ({ posts: state.posts }),
dispatch => ({ onGetAll: () => dispatch(getPosts()) })
);
This is a very simple pattern and I've used it on many applications
If you use react-router you can take advantage of onEnter hook.
Related
I prepared a boiled-down example on stackblitz:
https://stackblitz.com/edit/quasarframework-vy4eiw?file=README.md
The problem I try to resolve is this:
A quasar 2 app build with vite and vue 3 (and GSAP) uses layouts
Currently there are 2 layouts: StartpageLayout for the startpage at route ´/´and MainpageLayout for all the other pages at route ´/main´ and any children of it (/main/:child´`)
The MainpageLayout also contains the navigation menu
The navigation menu should be created (later on with an animation) when any route starting with ´/main´ is hit and destroyed, when there is a change to any other route
While navigating through any ´/main[/:child]´ route, the nav menu shall remain "stable" (not rebuild or anything like that)
The app uses 2 router-views for this, one in App.vue, one in MainLayout.vue. Changes between those states should mainly be handled in onBeforeRouteLeave and onBeforeRouteUpdate
To check, whether the app is in a "layout context", the routes have a meta.layoutKey, which is used in router guards to check, whether sth changed or not:
// Example: src/layouts/MainLayout.vue
onBeforeRouteUpdate((to, from, next) => {
console.log('%cMAIN_LAYOUT: onBeforeRouteUpdate invoked', consColRouter);
// compare meta.layoutKeys of routes
if (from.meta.layoutKey !== to.meta.layoutKey) {
console.warn(' YES, invoke router guard onBeforeRouteUpdate - animate!');
next() // this would be actually be called form an onComplete animation callback
} else {
console.log(' NOPE, do not invoke router guard onBeforeRouteUpdate');
next() // invoked as written
}
})
A pinia store manages state that (should) remember(s) activateMenu:
// Pinia store "pageTransitions.js" (composition API)
import { ref, reactive, computed } from 'vue'
import { defineStore } from 'pinia'
export const usePageTransitionsStore = defineStore('pageTransitions', () => {
// Pinia state in composition API
const pageTransitions = ref({
parent: false,
activateMenu: false
})
const setPageTransitions = (level, bool) => {
switch(level) {
case 'parent': {
pageTransitions.value.parent = bool
break
}
default: { console.log('default... must never happen!'); }
}
}
const setActivateMenu = (bool) => {
pageTransitions.value.activateMenu = bool
}
return {
pageTransitions,
setPageTransitions,
setActivateMenu
}
})
If store.pageTransitions.activateMenu is true, show the menu, if false, remove it. It is imported in MainLayout in order to use the activateMenu constant to manage the state of the nav menu. The onMount method sets this store variable to true. And it should be set to false in a ònBeforeRouteLeave`... (not yet implemented)
While the change from the startpage at ´/´to the MainPage at ´/main´ and vice versa works fine (even with animation, due to the store variable store.pageTransitions.parent), I keep having troubles with changes from ´/main´ to any child route ´/main/:child´ and vice versa. E.g. when the app is at /main and the user clicks on ´items 101´, the whole MainLayout is reloaded - also App.vue runs through its onAppear hooks again (see console) – and the nav is set to false again.
The goal is to not influence the MainLayout not its nested nav menu at all.
I wonder, why those reloads happen? MainLayout's onBeforeRoute checks against meta.layoutKey which does not change. But then I also observe that the pinia store gets loaded again, and the actiavteMenu var is set up false again...
Does anybody see my error(s)?
Ok, I believe I am VERY close to having my first working Vue JS application but I keep hitting little snag after little snag. I hope this is the last little snag.
I am using vue-async-computed and axios to fetch a customer object from my API.
I am then passing that property to a child component and rendering to screen like: {{customer.fName}}.
As far as I can see, the ajax call is being made and the response coming back is expected, the problem is there is nothing on the page, the customer object doesnt seem to update after the ajax call maybe.
Here is the profile page .vue file I'm working on
http://pastebin.com/DJH9pAtU
The component has a computed property called "customer" and as I said, I can see in the network tab, that request is being made and there are no errors. The response is being sent to the child component here:
<app-customerInfo :customer="customer"></app-customerInfo>
within that component I am rendering the data to the page:
{{customer.fName}}
But, the page shows no results. Is there a way to verify the value of the property "customer" in inspector? is there something obvious I am missing?
I've been using Vue for about a year and a half, and I realize the struggle that is dealing with async data loading and that good stuff. Here's how I would set up your component:
<script>
export default {
components: {
// your components were fine
},
data: () => ({ customer: {} }),
async mounted() {
const { data } = await this.axios.get(`/api/customer/get/${this.$route.params.id}`);
this.customer = data;
}
}
</script>
so what I did was initialize customer in the data function for your component, then when the component gets mounted, send an axios call to the server. When that call returns, set this.customer to the data. And like I said in my comment above, definitely check out Vue's devtools, they make tracking down variables and events super easy!
I believed your error is with naming. The vue-async-computed plugin needs a new property of the Vue object.
computed: {
customer: async function() {
this.axios.get('/api/customer/get/' + this.$route.params.id).then(function(response){
return(response.data);
});
}
}
should be:
asyncComputed: {
async customer() {
const res = await this.axios.get(`/api/customer/get/${this.$route.params.id}`);
return res.data;
}
}
I want to render SimpleModal component in handleClick , how can I achieve it through redux
can I do this way??
//ReactDOM.render(, document.getElementById("123"));
import React, { Component } from 'react';
import ReactDOM from 'react-dom'
import Redux,{createStore,combineReducers } from 'redux';
import SimpleModal from './modal.js';
import {Provider, connect} from 'react-redux';
import {displayItems} from './reducers.js';
const ecommerceAppReducer = require('./reducers.js').default;
const store = createStore(ecommerceAppReducer);
const EcommerceApp = React.createClass({
componentDidMount(){
store.dispatch({
type: 'LIST_DATA',
id: 12
});
},
handleClick: function(entity){
this.props.dispatch({
type: 'DISPLAY_INFORMATION',
entity:entity
});
**Want to render a SimpleModal here**
},
render() {
return (
<div>
<ul>{
this.props.state.displayItems.map(function(e) {
return <li><a onClick={this.handleClick.bind(this,e) }>{e.name}</a></li>
}.bind(this))
}
</ul>
</div>
);
}
});
const mapStateToProps = function (state) {
return {state};
}
const Eapp = connect(mapStateToProps)(EcommerceApp);
class App extends React.Component {
render() {
return (
<Provider store={store}>
<Eapp />
</Provider>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'));
In React, your render function should return what the app looks like currently. As in right now. React will take care of updating and rendering and so forth as long as you use one of the methods to inform React when it needs to rerender something.
One method is to call React.render on the root of your app. This is the worst way, but not terrible for small apps. Only recommended if you know what you're doing and even then there are probably better methods.
The next is to use setState() in your component. React will call that particular component's render method sometime after that. It's much more precise in that not your entire app gets rerendered (although you can always stop the rendering cascade by implementing shouldComponentUpdate judiciously).
Next is to call forceUpdate which is terrible to use unless you are really sure of what it is you're getting yourself into. React-Redux uses this because they do know what they're getting into.
And finally, there's React-Redux, which isn't really another way for React to render your component. But it is a new way for the developer. This is by far the recommended way to do things.
To use it, you just follow the connect prescribed method for transforming your Redux state into props for your component.
This requires reading the a Redux docs. It's a long and arduous process that is guaranteed to make anyone a better developer.
In your mapStateToProps implementation it's important to be very selective with what parts of the state you pass along to your component.
Don't just go and pass the entire Redux state. This would cause your entire app to rerender if anything at all changed anywhere in your app. Less than optimal. Only pass what you need. Don't even pass what child components need. They get their own connect.
Now onwards and forwards we go.
You want handleClick to pop up some stuffs and show it to the user.
Method 1: Use alert. It's ugly and super simple. It provides a terrible user experience so it's not recommended.
Method 2: Use React-Redux. dispatch an action that causes your reducer to put some data in the state that lets your app know to show the data. Looks like you are already doing that!
Redux will then inform React-Redux that something has changed.
React-Redux will then check if any of your components use the information in the state that was just changed. It knows what you use because this is what you returned from your mapStateToProps function.
React-Redux will then tell React to rerender any of the components that it finds need updating.
When your component's render method gets called, you'll get the new info in the props. So do:
render() {
return (
<div>
{Boolean(this.props.modalOpen) && <MyConnectedModal />}
<ul>{
this.props.displayItems.map(function(e) {
return <li key={e.name}><a onClick={this.handleClick.bind(this, e) }>{e.name}</a></li>
}.bind(this))
}
</ul>
</div>
);
}
There's still plenty wrong with the above code. You should, for instance, never bind in render.
Note that the modal is a component apart. It gets its data from React-Redux and not from props passed by the parent. This means your EcommerceApp component does not have to be responsible for updating the modal if any data it's displaying changes. Redux will take care of that. Actually with React-Redux's help of course. And React, naturally. Not necessarily in that order.
To recap what's going on here: Your render method tells React not what to pop up, but what the final result should look like. This is an enormous difference and pretty much the entire point of React.
You never tell React what changed. You always tell it what the final result should look like. React will then go and figure out what happened and will find an efficient way to show it in your browser window or electron or nw.js desktop app or native mobile app or anywhere else React worx.
I'm building an app with Meteor using the react-komposer package. It is very simple: There's a top-level component (App) containing a search form and a list of results. The list gets its entries through the props, provided by the komposer container (AppContainer). It works perfectly well, until I try to implement the search, to narrow down the results displayed in the list.
This is the code I've started with (AppContainer.jsx):
import { Meteor } from 'meteor/meteor';
import { composeWithTracker } from 'react-komposer';
import React, { Component } from 'react';
import Entries from '../api/entries.js';
import App from '../ui/App.jsx';
function composer(props, onData) {
if (Meteor.subscribe('entries').ready()) {
const entries = Entries.find({}).fetch();
onData(null, {entries});
};
};
export default composeWithTracker(composer)(App);
App simply renders out the whole list of entries.
What I'd like to achieve, is to pass query parameters to Entries.find({}).fetch(); with data coming from the App component (captured via a text input e.g.).
In other words: How can I feed a parameter into the AppContainer from the App (child) component, in order to search for specific entries and ultimately re-render the corresponding results?
To further clarify, here is the code for App.jsx:
import React, { Component } from 'react';
export default class App extends Component {
render() {
return (
<div>
<form>
<input type="text" placeholder="Search" />
</form>
<ul>
{this.props.entries.map((entry) => (
<li key={entry._id}>{entry.name}</li>
))}
</ul>
</div>
);
}
}
Thanks in advance!
I was going to write a comment for this to clarify on nupac's answer, but the amount of characters was too restrictive.
The sample code you're looking for is in the search tutorial link provided by nupac. Here is the composer function with the corresponding changes:
function composer(props, onData) {
if (Meteor.subscribe('entries', Session.get("searchValues")).ready()) {
const entries = Entries.find({}).fetch();
onData(null, {entries});
};
};
The solution is the session package. You may need to add it to your packages file and it should be available without having to import it. Otherwise try with import { Session } from 'meteor/session';
You just need to set the session when submitting the search form. Like this for instance:
Session.set("searchValues", {
key: value
});
The subscription will fetch the data automatically every time the specific session value changes.
Finally, you'll be able to access the values in the publish method on the server side:
Meteor.publish('entries', (query) => {
if (query) {
return Entries.find(query);
} else {
return Entries.find();
}
});
Hope this helps. If that's not the case, just let me know.
There are 2 approaches that you can take.
The Subscription way,
The Meteor.call way,
The Subscription way
It involves you setting a property that you fetch from the url. So you setup your routes to send a query property to you Component.Your component uses that property as a param to send to your publication and only subscribe to stuff that fits the search criteria. Then you put your query in your fetch statement and render the result.
The Meteor.call way
Forget subscription and do it the old way. Send your query to an endpoint, in this case a Meteor method, and render the results. I prefer this method for one reason, $text. Minimongo does not support $text so you cannot use $text to search for stuff on the client. Instead you can set up your server's mongo with text indexes and meteor method to handle the search and render the results.
See what suits your priorities. The meteor.call way requires you to do a bit more work to make a "Search result" shareable through url but you get richer search results. The subscription way is easier to implement.
Here is a link to a search tutorial for meteor and read about $text if you are interested
I want to implement dropdown with react and redux. Dropdown will be a part of other component, so, it it really "Dumb" component. But dropdown should call to api to fetch items, apply custom filters and etc. Api calls should be authenticated, tokens stored it global state. Should I pass tokens to component props? Or there is a better way to do this?
A dumb component, by definition, should be dumb: it means that it should take everything it needs "from the top", i.e. via the props.
Only a "Smart" ("connected" to Redux) Component, up the hierarchy, would deal with fetching the data using a new (async) Action, which would then modify the state when the API call returns, which would re-render your Dumb Component with its new props.
So in Redux:
Your Dumb component takes two props: one with the values coming from your API (which actually are part of your "state"), the other one a function that is called when your dropdown selected item changes. <MyDumbComponent data={this.props.data} onChange={this.onChange.bind(this)} />
Then a "Smart" component up the hierarchy will listen to that onChange function, and dispatch an Action (FETCH_DATA)
The (async) Action will call the API, and when receiving the data, call another Action (FETCH_DATA_SUCCESS) with the data
Then Redux, with a reducer, would update its state from the Action payload
Which will re-render your Component with its new props (new data), coming from the current state.
You might want to read this: http://redux.js.org/docs/basics/UsageWithReact.html
And regarding async actions: http://redux.js.org/docs/advanced/AsyncActions.html
Dumb component doesn't mean it can do anything like fetch updates, it means it's 'dumb' to the concept of redux, and knows nothing about your store or what library you're using. The benefit is that you can change your flux implementation and you only have the small bit of work to update the smart components.
For the type of scenario you're describing, you would give your <Menu> a function via props that would run when <Menu> wants to update the data. <Menu> knows nothing about Redux - it's just executing a function - but it's still able to fetch new data. Depending on the complexities, you could pass through the raw action creator (bound to dispatchAction) and have it run that.
import * as dataActions from './actions';
// We have dataActions.fetchItems() which is the action creater to get new items.
// Attach items, and the action creator (bound to dispatch) to props
#connect((store) => {
return {items: store.data.items}
}, dataActions)
class ItemsPage extends Component {
static propTypes = {
items: PropTypes.object, // the items from the store, from #connect
fetchItems: PropTypes.func // action dispatcher, from #connect
}
render() {
return (<Menu onChange={this.props.fetchItems} /> )
}
}
// Our 'dumb' component that doesnt know anything about Redux,
// but is still able to update data (via its smart parent)
class Menu extends Component {
static propTypes = {
onChange: PropTypes.func // same as fetchItems
}
render() {
return (<button onClick={this.props.onChange}>Update items</button>);
}
}