I have next object:
Book = {
id: 1,
titel: "Some Book",
sections: [
{id: 1,
titel: "Section Titel",
pages: [
{id: 1, content: "Some text"},
{id: 2, content: "Some text"},
{id: 3, content: "Some text"}
]
},
{id: 2,
titel: "Section Titel",
pages: [
{id: 1, content: "Some text"},
{id: 2, content: "Some text"}
]
}
]
}
Book object stored in Redux store (state.Book).
I have component which visualizes this Book object.
Component Book renders one or more Section components and each Section component renders Page component.
Book subscribed to Redux over connect function from react-redux and listens to store.Book, hole object.
It is clear that when titel of the Book will change then hole Book object will be re-rendered including all Sections and Pages.
There are two questions:
Will react engine really re-render Sections and Pages if only Book.titel changed? Or it will identify that other parts of the component do not changed and will not re-render them.
If it will re-render then what is the best way to avoid it? Should I subscribe Section and Page component to the Store also? But it will not solve the problem, because Book listens to hole state.Book object. Or should I subscribe Book component only to state.Book.id and state.Book.titel, then use this.context.store inside to path data to the inner components?
Thanks in advance.
Yes, React will call the render method in Sections and Pages but don't be worried about performance as those are really inexpensive operations. The Virtual DOM will abstract all the heavy operations (i.e. manipulating the DOM) minimizing the impact on performance. You can run some quick tests yourself to see how the UX is not affected at all by changes this size.
I would not recommend to prevent re-rendering (as I said in #1 it's an unexpensive operation) but if you really want to, you could use shouldComponentUpdate in the Section components. You just need to return false whenever you feel the Section or their child components don't need to render
Related
Imagine the following list:
Managing Director
Sales Director
IT Director
Technical Lead
Software Developer
Support Technician
HR Department
HR Officer
HR Assistant 1
HR Assistant 2
It's backed by a state in the form of:
[
{
id: 1,
text: "Managing Director",
children: [
{
id: 2,
text: "Sales Director"
}
...
]
}
...
]
Now I want to indent Support Technician. I would modify the state array to remove the item from the Technical Lead parent & add it to the Software Developer parent. The problem is, that React first deletes it, which causes all items below it to jump one line up, and then in the next frame adds it again to the new parent, which pushes those items a line down again. This appears as a flicker. It doesn't happen every time (sometimes react manages to render both in the same frame), but often enough it happens and is very distracting.
The state is modified in a way, that the parent passes its state callback setState down to its children. In this case, the initial state of the Technical Lead node looks like:
{
id: 4,
text: "Technical Lead",
children: [
{
id: 5,
text: "Software Developer"
},
{
id: 6,
text: "Support Technician"
}
]
}
As obvious from the state, every node renders all its children recursively.
After the indention, the state is modified to the following:
{
id: 4,
text: "Technical Lead",
children: [
{
id: 5,
text: "Software Developer",
chiilderen: [
{
id: 6,
text: "Support Technician"
}
]
}
]
}
If I were to this without React and instead with regular DOM APIs, I would move the node to the new parent with something like insertBefore(). React on the other hand unmounts & remounts the node.
Below is a simplified example of my "Node" component, which renders the list:
const Node = ({data, setSiblings}) => {
const [children, setChildren] = useState(data.children)
function indent() {
setSiblings(siblings => {
// const prevSibling = find the item in the state array
// const thisNode = {id, text, children}
const newPrevSibling = {...prevSibling, children: [thisNode]}
const siblingsWithout_ThisNode = deleteFromArray(siblings, thisNodeIndex)
// updateAtIndex() returns a new array with the modification (immutable)
return updateAtIndex(siblingsWithout_ThisNode, prevSiblingIndex, newPrevSibling)
})
}
const childNodes = children?.map(child =>
<Node data={child} setSiblings={setChildren} key={child.id}/>
)
return (
<li>
<div>{data.text}</div>
{childNodes ? <ul>{childNodes}</ul> : null}
</li>
)
}
The indent() function is triggered by a Tab press, but I didn't include the key handler logic here
I didn't find a solution to this problem directly, but I switched to using MST (MobX-State-Tree) for state management now and it worked with it (didn't flicker anymore - seemingly, both the unmounting & remounting of the component happen in the same frame now).
Below is a CodeSandbox with the MST implementation. When clicking e.g. on the Support Technician node, you can press Tab to indent and Shift + Tab to outdent (you have to click again, since it loses focus)
https://codesandbox.io/s/priceless-keldysh-17e9h?file=/src/App.js
While this doesn't answer my question directly, it helped solve the problem semantically (it's better than nothing).
Your answers have highlighted an oversight on my part. I was taking this pattern container/presentational way too seriously and this made me realize that we could do without <Component />.
Say I have two components, <ComponentContainer /> and <Component /> and the latter renders a <Table /> component. For the sake of an example, I use Table from Ant Design.
Now, <Table /> is expecting a property dataSource with the following shape:
const dataSource = [
{
key: '1',
name: 'Mike',
age: 32,
address: '10 Downing Street',
},
{
key: '2',
name: 'John',
age: 42,
address: '10 Downing Street',
},
];
Where should I initialize this variable? Do I have to set it in ComponentContainer and pass it down to <Component /> which just passes it further down to <Table> I initially passed down some unformatted data to <Component />, say (note: the shape might also be something completely different):
const data = {
{
name: 'Mike',
age: 32,
address: '10 Downing Street',
},
{
name: 'John',
age: 42,
address: '10 Downing Street',
}
}
and then declared and set that dataSource variable in <Component /> using this data:
const { data = [] } = props
const dataSource = data.map((pieceOfData, i) = > ({
...pieceOfData,
key: i
}))
<ComponentContainer > is not supposed to know that <Component /> is using a <Table /> so for me, it was ok to pass down some unformatted data to shape it then as needed further down the component tree even though we are in a presentational component. But I was told otherwise so I don't know. Is it really bad to do anything else than rendering?
This is known as prop-drilling, a process you go through to pass your data down in the React component tree. While it's not a bad pattern, there are some issues when trying to use it from such top-level components. Usually we want to keep logical pieces together and a clear distinction between presentational and logical components.
If you want to read more about it, I highly suggest Kent C. Dodds text on prop-drilling. The text has the potential issues and how to go around it, like using state management or React's Context API.
I recommend reading the beginning of this article by Dan Abramov https://medium.com/#dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0
It used to be that everyone lived and died by the presentational and container component idea and prop drilling. I tend to disagree with that model. I believe that the component logic and information should live as close as possible to where it's being used. It makes it easier for me to read and understand what code is doing if I don't have to constantly check back up 2 files to see what is being passed down. As stated in the other answer by Inacio there are good ways around this issue especially with the use of hooks.
I am somewhat new to React and looking for best practices for a particular situation within my React/Redux/Firebase PWA. Right now, the part I am concerned with is essentially a wrapper for the Yelp API.
I have a main component that queries the Yelp API when loaded (inside componentDidMount) based on user preferences received elsewhere in the app. This component also queries the API on a form submit with user inputs. It loops thru the data from both, and passes props to a child component. This view looks like a list of all the businesses received from the API. Here's what an example looks like:
"businesses": [
{
"rating": 4,
"price": "$",
"phone": "+14152520800",
"id": "E8RJkjfdcwgtyoPMjQ_Olg",
"alias": "four-barrel-coffee-san-francisco",
"is_closed": false,
"categories": [
{
"alias": "coffee",
"title": "Coffee & Tea"
}
],
"review_count": 1738,
"name": "Four Barrel Coffee",
"url": "https://www.yelp.com/biz/four-barrel-coffee-san-francisco",
"coordinates": {
"latitude": 37.7670169511878,
"longitude": -122.42184275
},
"image_url": "http://s3-media2.fl.yelpcdn.com/bphoto/MmgtASP3l_t4tPCL1iAsCg/o.jpg",
"location": {
"city": "San Francisco",
"country": "US",
"address2": "",
"address3": "",
"state": "CA",
"address1": "375 Valencia St",
"zip_code": "94103"
},
"distance": 1604.23,
"transactions": ["pickup", "delivery"]
},
// ...
],
As mentioned before, the main component passes down data to the child component, which renders each single object that list. This child component also creates a Link to another component based on the id of each business. This other component for now is simply the exact same look as one individual child component, just on a different URL. For example, the main component is "/venues" and the individual page for a business would be "/venue/E8RJkjfdcwgtyoPMjQ_Olg". The data is pulled from the Redux state (its a HOC), and filtered out to find that id.
The problem I'm running in to is when I refresh the page while on a businesses's individual page, Redux state is cleared out and hence is unable to render properly with the data. To try and work around this I attempted to have a service worker cache everything it would need, but this does not work. Refreshing ends up just showing the loading page I created, because its not making the call (which is expected - don't want it doing this) and its also not pulling data from the caches.
Is there a better way to accomplish getting data all the way to the individual business component after a refresh? Or a better way to cache the entire page/API response so it will render properly on a refresh?
I suppose I could have it reach out to the API for that specific business but I was trying to avoid that.
I remember Yelp API has an endpoint where you can get details of an individual business. That said, you should have a dynamic route for your individual business page; e.g. /businesses/:id. Then you can access this query param in your route props and go from there to fetch data for that specific business.
Something like this:
class IndividualBusiness extends Component {
// ...
componentDidMount() {
fetchBusinessById(this.props.match.id).then(setState(...))
// ...
}
// ...
}
The route would look like something like this:
<Route exact path="/businesses/:id" render={(routeProps) => <IndividualBusiness {...routeProps} />}/>
But what about when data is already in redux store? Simple, just add the control flow in your componentDidMount().
Have you tried localStorage? there's a redux middleware for it. It looks something like this:
import {createStore, compose} from 'redux';
import persist from 'redux-localstorage';
const store = createStore(reducer, compose(...otherMiddleware, persist(['apiResponses'])))
That should make the "apiResponses" section of your redux state persist through browser reloads.
Edit: Fixed JSFiddle Link
So i've been playing with Backbone and Marionette since a couple of weeks. I did some courses on Udemy, read the docs for both Backbone and Marionette. I can grasp most of the logic but somehow my mind can't wrap itself around the best way to approach a SPA I am trying to create.
API
So I have a rest api that returns some data:
http://localhost:3000/index
returns the following:
[
{
"id": 1,
"magazineTitle": "Title",
"magazineEditie": "Date",
"indexTitle": "Index Title",
"indexSubtitle": "Index Subtitle",
"mediaType": "image", //can also be "video"
"mainMedia": "https://source.unsplash.com/B0iF3I4bLBQ"
}
]
What I want
I want to be able to use this response and populate it over 2 seperate views.
one view will use the data to create a navigation bar
the other view will use it to create a hero header
What I can't seem to understand
Somehow I can't wrap my head around how this would be set up without making this 'illogical'
I feel like loading 2 views with the same model inside my Marionette.Application doesn't make any sense? Or the fact that I fetch my Collections and/or Models there...
I need some help clearing up some Layout issues and best practices I guess.
My code
Besides the fact that I get the data from a localhost url and I have my app setup with webpack this is more or less the code that I am using:
JSFiddle Demo
I have figured out what I needed to do. Based on the documentation (which was kind of confusing me) I figured out a way to render two views with it's needed data.
I was using a CollectionView to read a single data point (1 model) I somehow couldn't figure out a way to immediately get a single Model.
So far the model I had to do:
index.model.js
const IndexModel = Backbone.Model.extend({
urlRoot: "http://localhost:3000/index",
default: {
id: 1,
magazineTitle: "Mag Title",
magazineEditie: "Date",
indexTitle: "Title",
indexSubtitle: "Subtitle",
mediaType: "image",
mainMedia: "http://placehold.it/1900x800/",
},
});
The urlRoot argument here is what I need to do the exact call.
Then I was still confused how to structure my app but I ultimately used Regions and Marionette.View to setup the application.
App.js
export default Marionette.Application.extend({
region: "#content",
onBeforeStart() {
const router = new Router();
},
onStart() {
this.showView(new AppView());
},
});
app.view.js
const AppView = Marionette.View.extend({
tagName: "main",
id: "app",
template: template,
regions: {
navigationRegion: "#main-navigation",
appRegion: "#main-region",
pagesRegion: "#pages-region",
},
initialize() {
this.headerData = new IndexModel({ id: 1 });
this.pagesData = new PagesCollection();
},
onRender() {
this.showChildView("appRegion", new HeroView({ model: this.headerData, }));
this.showChildView("pagesRegion", new PagesView({ collection: this.pagesData, }));
},
});
I had to create a wrapping AppView that utilises regions to specify where child views should render.
I'm new to ReactJS and am unsure about the best place to put validation logic that is needed both by nested child components in my form, and the overall "parent" form component itself. Here is a over-simplified example that illustrates my question...
I have a object like this that represents a pet owner:
{
name: 'Jon Arbuckle',
pets: [
{ name: 'Odie', type: 'dog' },
{ name: 'Garfield', type: 'cat' }
]
}
I'm using a composite component called <PetOwnerForm> to render a form for editing this data. <PetOwnerForm> renders something like this:
<input type="text" value={name} />
<PetList value={petOwner.pets} />
<PetList> is a composite component that renders this:
<PetListItem value={this.props.value[i]} /> // Render this for each pet...
// buttons for adding/deleting pets
<PetListItem> renders something like this:
<input type="text" value={this.props.value.name} />
<PetTypePicker value={this.props.value.type} />
Lastly, <PetTypePicker> renders a <select> with <option>s for pet types.
<PetTypePicker> needs to know how to validate the selected type so it can display an inline error message (e.g., ensure that a value is selected).
However, <PetOwnerForm> also needs to know how to validate the pet type because it needs to know how to validate the entire object (on load, each time the form is updated, and before submitting the data back to the server). If any field is invalid, the "Save" button should be disabled.
So where, for example, should the "is a valid pet type selected?" logic go? (Bear in mind that this is a trivial example; in reality I have many fields like this and nested composite components).
The options I see so far are:
A) Replicate the validation logic for pet type (or whatever field) both in <PetOwnerForm> and <PetTypePicker>. This might just be a matter of calling the same, shared validation function in both places:
//PetOwnerForm.js:
validate(petOwnerObj) {
Util.isPetTypeValid(petOwnerObj.pets[i]) // for each pet
// validate the other properties in petOwnerObj...
}
//PetTypePicker.js:
validate(petType) {
Util.isPetTypeValid(petType)
}
B) Use custom PetOwner, Pet, and PetType models that have their own validators. This way you can always ask a model to validate itself, regardless of where it is. Maybe this would look something like this:
{
name: { value: 'Jon Arbuckle', isValid: ()=>{...} },
pets: [
{
name: { value: 'Garfield', isValid: ()=>{...} },
type: { value: 'cat', isValid: ()=>{...} }
},
...
]
}
C) Modify PetOwnerForm.js go recurse the pet owner object, validating each value, and setting an 'errors' property that child components can reference, resulting in an object like this:
{
name: { value: 'Jon Arbuckle asdfasdfasdf^^', errors: ['Too many characters', 'Contains invalid character']] },
pets: [
{
name: { value: '', errors: ['Required value missing'] },
type: { value: 'tree', errors: ['Invalid pet type'] }
},
...
]
}
Which option is recommended for React apps (or is there another option)?
It's a nice elaborate question. This question is not specific to ReactJS applications. It applies to all frameworks that follow component model.
Following are my recommendations:
Differentiate between action driven validation and data format validation.
Low level components are aware of data format they accept, so they must validate for it. For example, postal-code, email, phone, SSN etc have fixed formats and their corresponding components must validate for the right input format.
Low level components are not aware of actions being performed on the overall data. For example, selection of pet-owner-type can be mandatory for "create" pet-owner action but can be optional for "save draft" action. So, low level components which are not aware of end action must not perform action driven validations.
Action driven validation must be performed by the higher level component aware of action, for example PetOwnerForm. Such validation result must be notified to low level components so that they can display appropriate errors. Every low level component must have an error state to support it.