Dispatch to props only when clicked - javascript

The issue I am having is that the mapDispatchToProps is getting sent as a whole, where I want it to only send if I click on the button delete.
This is my class, its fetching the fetchList good, everything is working as expected but when I've added the delete button its seems to mess it all up, it seems to call the delete for every refresh on the page, any idea why?
Could it be the render() where I create the Button maybe it gets triggered without me clicking it? Just by creating the list, because it gets triggered for every occasion that each itemInList gets created via the map.
class List extends Component {
componentWillMount() {
this.props.fetchList();
}
componentWillReceiveProps(nextProps) {
if (nextProps.newItem) {
this.props.list.unshift(nextProps.newItem);
}
}
onDelete = (id) => {
this.props.deleteItem(id);
}
render() {
const listItems = this.props.list.map(itemInList => (
<div key={itemInList.id}>
<h3 className="title__font smaller">{itemInList.title}
<Button
btnType="Delete"
onClick={this.onDelete(itemInList.id)}>
<i className="fas fa-trash-alt"></i>
</Button>
</h3>
<p className="body__font">{itemInList.body}</p>
</div>
));
return (
<div>
<h1 className="title__font">List</h1>
{ listItems }
</div>
);
};
};
List.propTypes = {
fetchList: PropTypes.func.isRequired,
list: PropTypes.array.isRequired,
newItem: PropTypes.object
};
const mapStateToProps = state => ({
list: state.list.items,
newItem: state.list.item
});
const mapDispatchToProps = dispatch => {
return {
fetchList: () => dispatch( actions.fetchList() ),
deleteItem: (id) => dispatch( actions.deleteItem(id) )
};
};
export default connect(mapStateToProps, mapDispatchToProps)(List);
This is the actions for the delete item:
export const deleteItem = (id) => dispatch => {
console.log(id);
dispatch({
type: actionTypes.DELETE_ITEM,
payload: filtered
})
};
That log gets triggered 10 times in the actions file.

You're going to want to pass a function declaration into onClick and you'll need to pass the id in somehow. We don't want to declare any functions in the render method for performance issues, but we need some way to pass the id into the method upon invocation. Data attributes are a great solution to this problem.
Here is some relevant documentation.
React: Handling Events
HTMLElement.dataset
babel-plugin-proposal-class-properties
First, ensure the this context of your method is bound to the component as follows:
constructor(props) {
super(prop)
this.onDelete = this.onDelete.bind(this)
}
The above is required because class methods are actually defined on their prototype and not on individual instantiations. Side note: If you're using a build system that has something along the lines of babel-plugin-proposal-class-properties you could declare your method as follows:
onDelete = (e) => { this.props.deleteItem(e.target.dataset.id) }
You'll need to update your onDelete method as follows:
onDelete = (e) => {
this.props.deleteItem(e.target.dataset.id);
}
and you'll also need to update the Button markup in your render method like this:
<Button
btnType="Delete"
data-id={itemInList.id}
onClick={this.onDelete}
>
<i className="fas fa-trash-alt"></i>
</Button>
EDIT:
Here is a working Code Sandbox to demonstrate how it all works. I had to make some changes to your code such as excluding the non-included <Button/> component. I hope this helps you get where you need to be.

You're calling onDelete immediately, so it will dispatch on render.
Try replacing:
onDelete = (id) => {
this.props.deleteItem(id);
}
with
onDelete = (id) => () => this.props.deleteItem(id);

Related

React-Redux connecting two separate component with a toggle

I know this is probably a very basic question, but it's more of a "I don't understand the docs please help me" type of question.
I'm trying to connect two components using React-Redux: the first is a sidebar, and the second is a modal that should appear when clicking on a button in the sidebar. The components are not related in any parent-child relation (except root) so I assume redux is the best option.
I've read all the redux (and react-redux) docs and I understand the core concepts of redux, but I'm having trouble understanding how to implement them in my components.
Basically I want a button in the sidebar that toggles a stored state (true/false is enough) and according to that state the modal would appears (state==true => display:block) and disappear via a button in the modal (state==false => display:none).
What I think I need is an action to toggle a state, for example:
const modalsSlice = createSlice({
name: 'modals',
initalState,
reducers: {
toggleModal(state, action){
state = !state;
}
}
});
then connecting the action in both components (I'm writing the components in classes not as functions) by using:
const toggleModal = {type: 'modals/toggleModal', payload: ''};
const mapStateToProps = state => state.showHideModal;
export default connect(mapStateToProps, toggleModal)(Component);
Now, assuming I'm correct so far, I'm not sure how to continue. I.e. how am I suppose to receive and make the change in the components themselves? Sure, I need to put a function in a button with a onClick={foo} listener but how does the foo suppose to receive and handle the state? And am I suppose to initialize the showHideModal state somewhere? In the root component? While configuring the store?
Any help would be much appreciated.
State Initialisation
You are supposed to initialise the state showHideModal in the slice itself. Moreover, it should be named as either showModal or hideModal for a better interpretation of what this state does.
const modalSlice = createSlice({
name: 'modal',
initialState: {
showModal: false,
},
reducers: {
toggleModal(state){
state.showModal = !state.showModal;
}
}
});
export const { toggleModal } = modalSlice.actions;
SideBar Component
The onClick event handler needs to be passed explicitly via mapDispatchToProps.
import { toggleModal } from './modalSlice';
class Sidebar extends Component {
handleClick = () => {
const { toggleModal } = this.props;
toggleModal();
}
render() {
return (
<div>
{/* rest of JSX */}
<button onClick={this.handleClick}>Toggle Modal</button>
{/* rest of JSX */}
</div>
);
}
}
const mapDispatchToProps = {
toggleModal,
};
export default connect({}, mapDispatchToProps)(Sidebar);
Modal
Note: You cannot access property directly from state like you did state.showHideModal;. You need to access the slice first, followed by property present in it state.modal.showHideModal;.
class Modal extends Component {
handleClick = () => {
const { toggleModal } = this.props;
toggleModal();
}
render() {
const { showModal } = this.props;
return (
<>
{showModal ? (
<div>
<button onClick={this.handleClick}>Close</button>
</div>
) : null}
</>
);
}
}
const mapDispatchToProps = {
toggleModal,
};
const mapStateToProps = state => ({
showModal: state.modal.showModal,
});
export default connect(mapStateToProps, mapDispatchToProps)(Modal);
Update
Coming, to the the reason why Redux throws following warning:
A non-serializable value was detected in an action, in the path: payload
It's because a SyntheticEvent is being passed as a payload to the action. In order to fix this, you need to move the toggleModal call from the onClick prop to a separate handler function. For you reference, check the handleClick function in Modal and SideBar.

How to Save the value of a select option declared in componentDidmount method for use in all components

'm fetching an API from a source. And it was successful. But I want the value of the selected option to save when clicked, but whenever, I click on it, It shows and after I move to another section and move back to it, it would have reset itself.
I have a login form with steps, steps are like -> Next and previous button, just to save and navigate to next from till the last input, then submit. And I fetched an API with componentDidmount method. It worked so fine, but whenever I clicked on the select dropdown options and I choose an option, it worked, and If I move to the next step, other inputs will get saved but it will not be saved.
Here's my code.
//my componentDidmount method
constructor(props) {
super(props);
this.state = {
skills: null
};
}
fetchSkills = () => {
fetch('https://my_api_link')
.then(res => res.json())
.then(skills => {
this.setState({
skills: skills.map(s => s.ssp_skill_name)
})
});
};
componentDidMount() {
this.fetchSkills();
}
//how I used it
<div className="form-group mb-4" style={{maxWidth: "25rem"}}>
<label htmlFor="skills">Skills:</label>
{this.state.skills && (
<select className="form-control email_input">
{this.state.skills.map(s => (
option key={s}>{s}</option>
))}
</select>
)}
</div>
you can use one of the myriad of libs to support this case (useSWR, react-query, redux-toolkit, easy-peasy, and more and more). But if you wish to implement a solution without new deps, you can leverage context for that.
const skillsContext = React.createContext()
export default function SkillsContext(props) {
const [skills, setSkills] = React.useState([])
React.useEffect(() => {
fetch('https://my_api_link')
.then(res => res.json())
.then(skills => {
setSkills(skills.map(s => s.ssp_skill_name))
});
},[]}
return <skillsContext.Provider value={skills}>{props.children}</skillsContext.Provider>
}
export default useSkills = React.useContext(skillsContext)
place <SkillContext> at the root of your app, and then access the value with useSkills anywhere in your app.
if you're using the Component syntax, you can access it through a static contextType like so:
class SomeComponent extends Component {
static contextType = SkillsContext
render() {
this.context
}
}

React - change this.state onClick rendered with array.map()

I'm new to React and JavaScript.
I have a Menu component which renders an animation onClick and then redirects the app to another route, /coffee.
I would like to pass the value which was clicked (selected) to function this.gotoCoffee and update this.state.select, but I don't know how, since I am mapping all items in this.state.coffees in the same onClick event.
How do I do this and update this.state.select to the clicked value?
My code:
class Menus extends Component{
constructor (props) {
super(props);
this.state = {
coffees:[],
select: '',
isLoading: false,
redirect: false
};
};
gotoCoffee = () => {
this.setState({isLoading:true})
setTimeout(()=>{
this.setState({isLoading:false,redirect:true})
},5000)
}
renderCoffee = () => {
if (this.state.redirect) {
return (<Redirect to={`/coffee/${this.state.select}`} />)
}
}
render(){
const data = this.state.coffees;
return (
<div>
<h1 className="title is-1"><font color="#C86428">Menu</font></h1>
<hr/><br/>
{data.map(c =>
<span key={c}>
<div>
{this.state.isLoading && <Brewing />}
{this.renderCoffee()}
<div onClick={() => this.gotoCoffee()}
<strong><font color="#C86428">{c}</font></strong></div>
</div>
</span>)
}
</div>
);
}
}
export default withRouter(Menus);
I have tried passing the value like so:
gotoCoffee = (e) => {
this.setState({isLoading:true,select:e})
setTimeout(()=>{
this.setState({isLoading:false,redirect:true})
},5000)
console.log(this.state.select)
}
an like so:
<div onClick={(c) => this.gotoCoffee(c)}
or so:
<div onClick={(event => this.gotoCoffee(event.target.value}
but console.log(this.state.select) shows me 'undefined' for both tries.
It appears that I'm passing the Class with 'c'.
browser shows me precisely that on the uri at redirect:
http://localhost/coffee/[object%20Object]
Now if I pass mapped 'c' to {this.renderCoffee(c)}, which not an onClick event, I manage to pass the array items.
But I need to pass not the object, but the clicked value 'c' to this.gotoCoffee(c), and THEN update this.state.select.
How do I fix this?
You can pass index of element to gotoCoffee with closure in render. Then in gotoCoffee, just access that element as this.state.coffees[index].
gotoCoffee = (index) => {
this.setState({isLoading:true, select: this.state.coffees[index]})
setTimeout(()=>{
this.setState({isLoading:false,redirect:true})
},5000)
}
render(){
const data = this.state.coffees;
return (
<div>
<h1 className="title is-1"><font color="#C86428">Menu</font></h1>
<hr/><br/>
{data.map((c, index) =>
<span key={c}>
<div>
{this.state.isLoading && <Brewing />}
{this.renderCoffee()}
<div onClick={() => this.gotoCoffee(index)}
<strong><font color="#C86428">{c}</font></strong></div>
</div>
</span>)
}
</div>
);
}
}
so based off your code you could do it a couple of ways.
onClick=(event) => this.gotoCoffee(event.target.value)
This looks like the approach you want.
onClick=() => this.gotoCoffee(c)
c would be related to your item in the array.
All the answers look alright and working for you and it's obvious you made a mistake by not passing the correct value in click handler. But since you're new in this era I thought it's better to change your implementation this way:
It's not necessary use constructor at all and you can declare a state property with initial values:
class Menus extends Component{
state= {
/* state properties */
};
}
When you declare functions in render method it always creates a new one each rendering which has some cost and is not optimized. It's better if you use currying:
handleClick = selected => () => { /* handle click */ }
render () {
// ...
coffees.map( coffee =>
// ...
<div onClick={ this.handleClick(coffee) }>
// ...
}
You can redirect with history.replace since you wrapped your component with withRouterand that's helpful here cause you redirecting on click and get rid of renderCoffee method:
handleClick = selected => () =>
this.setState(
{ isLoading: true},
() => setTimeout(
() => {
const { history } = this.props;
this.setState({ isLoading: false });
history.replace(`/${coffee}`);
}
, 5000)
);
Since Redirect replaces route and I think you want normal page change not replacing I suggest using history.push instead.
You've actually almost got it in your question. I'm betting the reason your state is undefined is due to the short lived nature of event. setState is an asynchronous action and does not always occur immediately. By passing the event off directly and allowing the function to proceed as normal, the event is released before state can be set. My advice would be to update your gotoCoffee function to this:
gotoCoffee = (e) => {
const selectedCoffee = e.target.value
this.setState({isLoading:true,select:selectedCoffee},() =>
{console.log(this.state.select})
setTimeout(()=>{
this.setState({isLoading:false,redirect:true})
},5000)
}
Note that I moved your console.log line to a callback function within setState so that it's not triggered until AFTER state has updated. Any time you are using a class component and need to do something immediately after updating state, use the callback function.

Redux state not updating with javascript object

I have this container and component and the object yeast that Im trying to put in my store. However, its not working whenever I try and save it. The object looks like this
{ yeastType : value, temp: value}
Container.js
const mapDispatchToProps = (dispatch) => {
return {
handleYeastChange: (yeast) => {
dispatch(actions.updateYeast(yeast))
}
}
};
const RecipeYeastContainer = connect(
mapStateToProps,
mapDispatchToProps
)(RecipeYeast);
Component.js
updateYeastState = (updatedYeast) => {
this.props.handleYeastChange(updatedYeast)
};
I have no errors in the console or anything. When I open my redux dev tools its telling me the state has already been updated by the time the action is called. And thus only ever saving the first letter thats input into my field. It never persists it. Its really weird. Its also never showing up in the UI. I can type as much as I want and see the action firing and the state keeping the first letter but its not showing up in the input.
Whats weird is that when I change the code to pass in both yeastType and temp to the property function and construct the object in there it works. (See below)
This works: Container.js
const mapDispatchToProps = (dispatch) => {
return {
handleYeastChange: (yeastType, temp) => {
const yeast = {yeastType, temp}
dispatch(actions.updateYeast(yeast))
}
}
};
Component.js
updateYeastState = (updatedYeast) => {
this.props.handleYeastChange(updatedYeast.yeastType, updatedYeast.temp)
};
I cannot figure out why this is happening. I thought I could just pass the object all the way through and not have to reconstruct it.
Do you dispatch your action correctly? And in using redux, you are not updating the state of the component, you're updating the store and then the value in component is from your mapStateToProps function that get from the store. Say it you're updating your store with the object named yourReducer store. ex:
Container.js:
const mapStateToProps = (state) => ({
inputValue: state.yourReducer.value
})
const mapDispatchToProps = (dispatch) => ({
inputHandler: (e) => {
dispatch(yourAction({type: 'CHANGE_INPUT', value: e.target.value}))
// The store catches the action and pass to the reducer that can read the action's type
// in `yourReducer` will catch the 'CHANGE_INPUT' type and
// return a new value as same as the value that passed from the action
}
})
export default connect(mapStateToProps)(Component)
Component.js
export default class Component extends React.Component {
{ your custom function here ... }
render() {
const { inputValue, inputHandler } = this.props
return (
<div>
{/* The input will be the same as the inputValue changed */}
<input value={inputValue} onChange={inputHandler} />
</div>
)
}
}
For debugging redux you can try this redux-devtool.

Test a function that calls an API

I need to test the fetchData() function. I have been trying to follow this (and many other) tutorials and trying to get it to work with my function for the past 3 hours but no luck so far. I'd preferably want to do it another way anyway because I don't think jQuery and React should be used together.
I essentially want to check if 1) the function is called when the search form has been submitted (button within SearchForm clicked) and 2) check if the data comes back.
Does anyone have any suggestions on how to get started please?
Thanks
Home
export default class Home extends Component {
constructor() {
super();
this.state = {
value: '',
loading: false,
dataError: false
}
this.nodes = [];
this.fetchData = this.fetchData.bind(this);
}
fetchData(e) {
e.preventDefault();
this.setState({loading: true});
axios.get(`https://api.github.com/search/repositories?q=${this.state.value}`)
.then(res => {
this.nodes = res.data.items.map((d, k) => <RepoItem {...d} key={k}/>);
this.setState({loading: false});
})
.catch(err => {
this.setState({dataError: true});
});
}
render() {
return (
<div className="home-wrapper">
<SearchForm value={this.state.value}
onSubmit={this.fetchData}
onChange={(e) => this.setState({value: e.target.value})}/>
{this.state.loading ?
<Spinner/>:
!this.state.dataError ? this.nodes :
<h1>Oops! Something went wrong, please try again!</h1>}
</div>
);
}
}
RepoItem
export const RepoItem = props => (
<div className="repo-item">
<h1>{props.full_name}</h1>
</div>);
To check if the function is called upon form submission, you can shallow-render the <SearchForm> component with the spy function passed as a onSubmit prop. Simulate the submit event and check if the spy function is called.
To check if the data comes back, mock the axios service. You can use this library to mock axios calls. Call the fetchData() and see if the this.nodes and state updated correctly.
const wrapper = shallow(<Home />);
wrapper.instance().fetchData().then(() => {
... your assertions go here ...
})
I think it's always the best practice to return a Promise object or any chainable object from a method where asynchronous action takes place.

Categories

Resources