How to manage state between React router and redux - reactjs

I have data that I'd be fetching from an api stored as items,
{ '001': {id: '001', name:'foo',color:'blue' }}, '002': { id: '002',name:'bar',color:'orange' }, '003': { id: '003',name:'baz',color:'pink' }}
which would be added to my redux state tree and would determine what's rendered on a particular path. For example, going to /foo would directed a user to a page with a background color of its associated color, blue. I have some code I started on trying to implement this but am confused as to how I'd coordinate this. I was looking at possibly doing something like running Object.keys(props.items).find(id => props.items[id].name === match.params.name), but figure that this wouldn't be a super scalable solution and would lead to a loss of the O(1) lookup afforded by having an object. The code is here: https://codesandbox.io/s/n562wzzmkm

If you want to use the name property as the path to each item, then the method of matching (searching/finding) that you mentioned is, in my mind, probably the best option. Because name is down one level for each item, we have to search all of our items for a match to match.params.id onClick in order to render a <Detail/>s component with the correct item's data.
But, if you're open to it, I'd propose you use each item's top-level key (i.e. 001, 002, 003) as the the to prop for your mapped <Link/> components. In that way, we can still achieve O(1) by using that method in conjunction with the render prop for your <Route/> that handles rendering a <Detail/> component with the chosen item's data. We'll still do the lookup using match.params.id, but we'll only have to be looking at the top level of items to find the correct item and successfully set the correct background color.
The code in question below and a fork of your CodeSandbox: https://codesandbox.io/s/xl8von46nw?fontsize=14
/**** Setting each to prop equal to the top-level id for each item ****/
<ul>
{Object.keys(items).map(id => (
<li key={id}>
<Link to={id}>item {items[id].name}</Link>
</li>
))}
</ul>
/**** I used connect() in routes.js to access items ****/
<Route
path="/:id"
render={({ match }) => (
<Detail
item={items[match.params.id]}
/>
)}
/>
Hope that helps or is an acceptable solution!

Related

React Stateless Components: Interacting with their output and appearance

I have looked around for an answer to this - the closest I found being this question - but there is I think a significant difference in my case (the fact that it starts to get into the parent holding the state of its children's... children) which has finally lead to me asking for some clarification.
A very simple example of what I mean is below (and will hopefully better illustrate what I'm asking):
Suppose we have a bunch of book documents like
bookList = [
{
title: "book 1",
author: "bob",
isbn: 1,
chapters: [
{ chapterNum: 1, chapterTitle: "intro", chapterDesc: "very first chapter", startPg: 2, endPg: 23 },
{ chapterNum: 2, chapterTitle: "getting started", chapterDesc: "the basics", startPg: 24, endPg: 45 }
]},
{
title: "book 2" ... }
]
So main point being these embedded objects within documents that could be very long and as such may be collapsed / expanded.
And then here is a rough sample of code showing the components
class App extends Component {
constructor(props) {
super(props);
this.state = {
books: bookList,
focusBook: null
}
this.updateDetailDiv = this.updateDetailDiv.bind(this);
}
updateDetailDiv(book) {
this.setState(
{ focusBook: book}
);
}
render() {
return(
<BookList
bookList = {this.state.books}
updateDetailDiv = { this.updateDetailDiv }
/>
<BookDetail
focusBook = { this.state.focusBook }
/>
);
}
}
const BookList = props => {
return (
props.bookList.map(item=>
<li onClick={()=> props.updateDetailDiv(item)}> {item.title} </li>
)
);
}
const BookDetail = props => {
return (
<div className="bookDetails">
{ props.focusBook != null
? <div>
{props.focusBook.title},
{props.focusBook.author},
{props.focusBook.isbn}
Chapters:
<div className="chapterList">
{ props.focusBook.chapters.map(item=>
<span onClick={()=>someFunction(item)}>{item.chapterNum} - {item.chapterName}</span>
)}
</div>
<div id="chapterDetails">
This text will be replaced with the last clicked chapter's expanded details
</div>
</div>
: <div>
Select A Book
</div>
})
}
someFunction(item) {
document.getElementById('chapterDetails').innerHTML = `<p>${item.chapterDesc}</p><p>${item.startPg}</p><p>${item.endPg}</p>`;
}
So my problem is that i'm not sure what the best approach is for handling simple cosmetic / visual changes to data in functional stateless components without passing it up to the parent component - which is fine and makes sense for the first child - but what happens when many children will have their own children (who may have their own children) --> all requiring their own rendering options?
For example - here the App component will re-render the DetailDiv component (since the state has changed) - but I don't want the App also handling the DetailDiv's detailed div. In my example here its all very simple but the actual application I'm working on has 2 or 3 layers of embedded items that - once rendered by App - could realisticially just be modified visually by normal JS.
SO in my example you'll see I have a someFunction() in each Chapter listing - I can make this work by writing a separate simple 'traditional JS DOM function' (ie: target.getElementById or closest() -- but i don't think i'm supposed to be using normal JS to manipulate the DOM while using React.
So again to summarize - what is the best way to handle simple DOM manipulation to the rendered output of stateless components? Making these into their own class seems like overkill - and having the 'parent' App handle its 'grandchildren' and 'great-grandchildren's state is going to be unwieldy as the Application grows. I must be missing an obvious example out there because I haven't seen much in the way of handling this without layers of stateful components.
EDIT for clarity:
BookDetail is a stateless component.
It is handed an object as a prop by a parent stateful component (App)
When App's state is changed, it will render again, reflecting the changes.
Assume BookDetail is responsible for displaying a lot of data.
I want it so each of the span in BookDetail, when clicked, will display its relevant item in the chapterDetail div.
If another span is clicked, then the chapterDetail div would fill with that item's details. (this is just a simple example - it can be any other pure appearance change to some stateless component - where it seems like overkill for a parent to have to keep track of it)
I don't know how to change the UI/appearance of the stateless component after it is rendered without giving it state OR making the parent keep track of what is essentially a 'substate' (since the only way to update the appearance of a component is to change its state, triggering a render).
Is there a way to do this without making BookDetail a stateful component?
You can add a little bit of simple state to functional components to track the selected index. In this case I would store a "selected chapter index" in state and then render in the div the "chapters[index].details", all without manipulating the DOM which is a React anti-pattern.
The use-case here is that the selected chapter is an internal detail that only BookDetail cares about, so don't lift this "state" to a parent component and since it is also only relevant during the lifetime of BookDetail it is rather unnecessary to store this selected index in an app-wide state management system, like redux.
const BookDetail = ({ focusBook }) => {
// use a state hook to store a selected chapter index
const [selectedChapter, setSelectedChapter] = useState();
useEffect(() => setSelectedChapter(-1), [focusBook]);
if (!focusBook) {
return <div>Select A Book</div>;
}
const { author, chapters, isbn, title } = focusBook;
return (
<div className="bookDetails">
<div>
<div>Title: {title},</div>
<div>Author: {author},</div>
<div>ISBN: {isbn}</div>
Chapters:
<div className="chapterList">
{chapters.map(({chapterName, chapterNum}, index) => (
<button
key={chapterName}
onClick={() => setSelectedChapter(selectedChapter >= 0 ? -1 : index)} // set the selected index
>
{chapterNum} - {chapterName}
</button>
))}
</div>
// if a valid index is selected then render details div with
// chapter details by index
{chapters[selectedChapter] && (
<div id="chapterDetails">
{chapters[selectedChapter].details}
</div>
)}
</div>
</div>
);
};
DEMO
There is some approaches you can do to solve this problem.
First, you don't need to create some class components for your functional components, instead, you can use react hooks, like useState so the component can control it's own content.
Now, if you don't want to use React Hooks, you can use React Redux store to manage all your states: you can only change the state values using the Redux actions.
Happy coding! :D

passing mapped data from an api to another component

so i have data that i get from an api in componentDidMount, and then i map over it in the render. lets say the map returns 4 objects. How do i create a button click event that captures that specific objects data, and passes it along a route to another component?
code:
clickEvent(){
???
}
this.example = this.state.data.slice(1).map((data, key) =>
<div key={item.Id}>
<div>{data.dataIWantToPass}</div>
<Link to='/next_component_path' onClick={clickEvent}}>Click</Link>
</div>
So lets say the above returns 4 objects. I want the third one, when the link is clicked to pass its data.
clickEvent(dataUWantToPass){
<NewComponent dataPassed={dataUWantToPass} />
}
this.example = this.state.data.slice(1).map((data, key) =>
<div key={data.Id}>
<div>{data.dataIWantToPass}</div>
//Link is a react-router-dom component, this helps you to redirect to other component (to which you have added the route for
<Link to='/next_component_path'>
<button onClick={()=>this.clickEvent(data.dataIWantToPass)} value="Click"/>
</Link>
</div>
))
You can receive the data in the NewComponent as props.
If you want to check than you can write the componentWillReceiveProps() method in the NewComponent as:
componentWillReceiveProps(reveivedProps){
console.log(reveivedProps);
}
Ok, I have solved this by piecing together various bits from comments, so thank you for your contributions. This is how it was resolved:
On the link tag i did this:
<Link to={{ pathname: '/path', query:{passed_id: data.id} }}></Link>
Then on the component that the path routes to:
this.setState({passed_id: this.props.location.query.passed_id});
And that gave me access to the data i was passing, in this case an ID. Now I just need to figure out how to compare that ID, with the looped data I also pass (via local storage), and map over it. Another question i suppose.
You need to use redux. This allows you to store data in global application store, and get the data from any component which is subscribed to the store. Today almost every react project needs redux to manage the data across the application

How do I call an event handler or method in a child component from a parent?

I'm trying to implement something similar to the Floating Action Button (FAB) in the Material-UI docs:
https://material-ui.com/demos/buttons/#floating-action-buttons
They have something like:
<SwipeableViews>
<TabContainer dir={theme.direction}>Item One</TabContainer>
<TabContainer dir={theme.direction}>Item Two</TabContainer>
<TabContainer dir={theme.direction}>Item Three</TabContainer>
</SwipeableViews>
{
fabs.map((fab, index) => (
<Zoom>
<Fab>{fab.icon}</Fab>
</Zoom>
));
}
I have something like:
<SwipeableViews>
<TabContainer dir={theme.direction}>
<ListOfThingsComponent />
</TabContainer>
<TabContainer dir={theme.direction}>Item Two</TabContainer>
<TabContainer dir={theme.direction}>Item Three</TabContainer>
</SwipeableViews>
{
fabs.map((fab, index) => (
<Zoom>
<Fab onClick={ListOfThingsComponent.Add???}>
Add Item to List Component
</Fab>
</Zoom>
));
}
My ListOfThingsComponent originally had an Add button and it worked great. But I wanted to follow the FAB approach for it like they had in the docs. In order to do this, the Add button would then reside outside of the child component. So how do I get a button from the parent to call the Add method of the child component?
I'm not sure how to actually implement the Add Item to List click event handler given that my list component is inside the tab, while the FAB is outside the whole tab structure.
As far as I know I can either:
find a way to connect parent/child to pass the event handler through the levels (e.g. How to pass an event handler to a child component in React)
find a way to better compose components/hierarchy to put the responsibility at the right level (e.g. remove the component and put it in the same file with this in scope using function components?)
I've seen people use ref but that just feels hacky. I'd like to know how it should be done in React. It would be nice if the example went just a bit further and showed where the event handling should reside for the FABs.
thanks in advance, as always, I'll post what I end up doing
It depends on what you expect the clicks to do. Will they only change the state of the given item or will they perform changes outside of that hierarchy? Will a fab be present in every single Tab or you're not sure?
I would think in most cases you're better off doing what you were doing before. Write a CustomComponent for each Tab and have it handle the FAB by itself. The only case in which this could be a bad approach is if you know beforehand that the FAB's callback will make changes up and out of the CustomComponent hierarchy, because in that case you may end up with a callback mess in the long run (still, nothing that global state management couldn't fix).
Edit after your edit: Having a button call a function that is inside a child component is arguably impossible to do in React (without resorting to Refs or other mechanisms that avoid React entirely) because of its one-way data flow. That function has to be somewhere in common, in this case in the component that mounts the button and the ListOfThings component. The button would call that method which would change the state in the "Parent" component, and the new state gets passed to the ListOfThings component via props:
export default class Parent extends Component {
state = {
list: []
};
clickHandler = () => {
// Update state however you need
this.setState({
list: [...this.state.list, 'newItem']
});
}
render() {
return (
<div>
<SwipeableViews>
<TabContainer dir={theme.direction}>
<ListOfThingsComponent list={this.state.list /* Passing the state as prop */}/>
</TabContainer>
<TabContainer dir={theme.direction}>Item Two</TabContainer>
<TabContainer dir={theme.direction}>Item Three</TabContainer>
</SwipeableViews>
{
fabs.map((fab, index) => (
<Zoom>
<Fab onClick={this.clickHandler /* Passing the click callback */}>
Add Item to List Component
</Fab>
</Zoom>
))
}
</div>
)
}
}
If you truly need your hierarchy to stay like that, you have to use this method or some form of global state management that the ListOfThingsComponent can read from.

Redirect to other page based on datalist option(Reactjs)

I want to link to other page when one of them is selected.
`
{
this.state.datas.map((data,i)=>(
<option key={i} value={data.display_name}><Link to={"/search?q="+encodeURI (data.name)} onClick={(e)=>this.clearData(data.name)}></Link></option>
))
}
</datalist>`
I've tried several options, including nesting option inside Link and vice versa, but pure html dropdowns don't seem to work well with React Router.
Here's a simple fix, though:
this.state.datas.map((data,i) =>
<option
key={i}
value={data.display_name}
onClick={(e) => {this.clearData(data.name); this.props.history.push("/search?q=" + encodeURI(data.name)) }}
>
{data.name} // Perhaps you were missing the stuff between the <option> tags too?
</option>)
If you've connected your component to the Router properly, you will have the history object in the component's props and be able to call history.push which will redirect the user to the newly specified route.
This creates an event listener on each dropdown option, so if you have lots of data, somewhat more performant approach would be to create another event handler in your class and attach it to the onChange listener of the select element.
onChange = (event, data) => {
this.clearData(data.name);
this.props.history.push("/search?q=" + encodeURI(data.name));
}
One more thing, avoid using index i as key because it can lead to unexpected issues with rendering. Try to find a value that's globally unique for the element you're rendering. For example, data.data_id would be a good key if you happen to have specified one in your database.
More on dangers of index as key

ReactiveSearch components and passing state/props

I have a ReactiveList component that lives in my SearchContainer. All it does is display search results based on user query. The ReactiveList component works, it automatically displays the contents of my ES index (hosted at Appbaseio). It DOES NOT display results based on user submitted query.
In my global Navbar I have a DataSearch component (conditionally rendered based on route) that provides autocomplete. I also have a pretty much identical DataSearch component in my homepage at ('/').
I'm trying to figure out how to pass the query from DataSearch to ReactiveList so that the results displayed are actually based on a user submitted query?
So let me reiterate, I'm trying to figure out how to get DataSearch and ReactiveList to talk to each other.
In my DataSearch component, I have
<DataSearch
componentId="q"
URLParams={true}
onValueSelected={(value, cause, source) => {
if (cause !== 'SUGGESTION_SELECT') {
<Redirect to={{
pathname: '/search',
search: `?q=${value}`
}}/>;
} else {
this.valueSelected = true;
}
}}
...
/>
In my ReactiveList component I have:
<ReactiveList
componentId="q"
URLParams={true}
onNoResults="Sorry, no results were found, try searching again."
react={{ or: ["q"] }}
onData={(res) => { ... }}
...
/>
From what I understand, as long as I have URLParams={true} in DataSearch and componentId='q' in both DataSearch and ReactiveList - search should work. Autocomplete seems to be functional but ReactiveList is NOT displaying results based on user query.
Do I need to add another property to DataSearch or ...? Where am I going wrong.
Different components should not have the same ID. Set the componentId for your ReactiveList to something unique (and choose a different unique ID for your DataSearch).
Not sure if there is another problem in the setup but this is something you can fix right away.

Resources