React: button with spinner, how to know when the animation should stop? - javascript

I would like to create a custom button component that shows a loading spinner within itself when it's pressed and with a condition that can be externally defined which will tell the button to remove the spinner and return to its original appearance. Something like this:
<CustomButton
type="button"
className="btn btn-primary"
stopSpinningWhen={condition}
onClick={() => ...}
>
Click me
</CustomButton>
Currently, my buttons with a spinner look like this, which is super fine, but it's a pain to write repetitive code/states for each single button:
const [buttonSpinner, setButtonSpinner] = useState(false);
const onClickEvent = (ev) => {
setButtonSpinner(true);
if (condition) {
setButtonSpinner(false);
}
};
return (
<button
type="button"
className="btn btn-primary"
onClick={onClickEvent}
disabled={buttonSpinner}
>
{buttonSpinner ? (
<span
className="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
></span>
) : (
"Click me"
)}
</button>
);
I'm using React 17.0.2.
Is it even possible?

You can create your own custom button that receives isLoading additionally.
const Spinner = (
<span
className="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
/>
)
const CustomButton = (props) => (
<button
type="button"
className="btn btn-primary"
onClick={props.onClick}
disabled={props.isLoading}
>
{props.isLoading ? <Spinner /> : "Click me"}
</button>
)
const YourComponent = () => {
const [isLoading, setIsLoading] = useState(false)
const onClick = async (event) => {
setIsLoading(true)
doHeavyTask()
setIsLoading(false)
}
return (
<div>
<CustomButton isLoading={isLoading} onClick={onClick} />
</div>
)
}

You use a loading property as your condition and just pass that as a prop to your custom button component. Something like this:
const myComponent = () =>{
const [loading, setLoading] = useState(false)
const myFunc = async () = {
setLoading(true)
//call api or do seomthing, after the process finishes set loading to false again
const resp = await fetch("myAPIURL")
console.log(resp.data)
setLoading(false)
}
return(
<CustomButton
type="button"
className="btn btn-primary"
spinning={loading}
onClick={() => myFunc()}
>
Click me
</CustomButton>
)
}

Related

Display a notification that an item was successfully deleted

The site has a button for deleting an element (one element is deleted per button click).
Timing: the user presses the delete button -> a window opens with a warning and two buttons: cancel and confirm -> when the confirm button is pressed, the deletion process begins, which is accompanied by a scroll wheel. After deletion, the window closes and you can continue to work on the site.
I would like to display a notification after the window is closed that the item was removed successfully. Tell me how can I do it.
export function Delete() {
const [alertShown, setAlertShown] = useState(false);
const [alertLoading, setAlertLoading] = useState(false);
const onNo = () => {
setAlertShown(false);
};
const onYes = async () => {
setAlertLoading(true);
await deleteItem();
setAlertShown(false);
setAlertLoading(false);
};
return <ButtonGroup >
<div onClick={() => setAlertShown(true)}>
<DeleteForeverIcon/>
</div>
{alertShown && (
<Dialog open={onYes}>
{alertLoading
? <div ><Spinner/></div>
: <DialogActions >
<Button color="error" onClick={onNo}>Cancel</Button >
<Button onClick={onYes}>Confirm </Button >
</DialogActions>}
</Dialog>
)}
</ButtonGroup>
}
The easiest approach to implement notification is using setTimeout. You can try the below code snippet
export function Delete() {
const [alertShown, setAlertShown] = useState(false);
const [alertLoading, setAlertLoading] = useState(false);
const [notificationShown, setNotificationShown] = useState(false);
const onNo = () => {
setAlertShown(false);
};
const onYes = async () => {
setAlertLoading(true);
await deleteItem();
setAlertShown(false);
setAlertLoading(false);
//open the notification
setNotificationShown(true);
setTimeout(() => {
setNotificationShown(false);
}, 5000); //automatically close the notification after 5 seconds
};
return (
<ButtonGroup>
{notificationShown && <span>The item was removed successfully</span>}
<div onClick={() => setAlertShown(true)}>
<DeleteForeverIcon />
</div>
{alertShown && (
<Dialog open={onYes}>
{alertLoading ? (
<div>
<Spinner />
</div>
) : (
<DialogActions>
<Button color="error" onClick={onNo}>
Cancel
</Button>
<Button onClick={onYes}>Confirm </Button>
</DialogActions>
)}
</Dialog>
)}
</ButtonGroup>
);
}
The sandbox link

React: Uncaught TypeError: X is not a function

I am trying to render a button in a react-table column. If I refactor the button to its own component, it complains that it's not a function.
Top level (ExampleReactTable.js):
const handleClick = () => setIsOpen(true);
const columns = getTableColumns(handleClick);
Middle layer (getTableColumns.js):
Cell: () => <ExampleButton handleClick={() => handleClick()} />
Botton layer (ExampleButton.js):
const ExampleButton = handleClick => {
console.log(handleClick);
return (
<Box>
<Button variation="text" onClick={() => handleClick()}>
Click
</Button>
</Box>
);
};
This errors out with handleClick is not a function.
If I don't refactor the button out, it works:
Cell: () => (
<Box>
<Button variation="text" onClick={() => handleClick()}>
Click
</Button>
</Box>)
You are not destructuring props, because the function received the props as an object, which you are passing to onclick as a function, resulting in the error, try this:
const ExampleButton = ({handleClick}) => {
console.log(handleClick);
return (
<Box>
<Button variation="text" onClick={() => handleClick()}>
Click
</Button>
</Box>
);
};
This is the same as:
const ExampleButton = (props) => {
console.log(handleClick);
return (
<Box>
<Button variation="text" onClick={() => props.handleClick()}>
Click
</Button>
</Box>
);
};

Dispatch a Redux action from a modal in React

I'm a begginer in React and I'm trying to find a way to dispatch a redux action from a modal.
I have a list of products and a button 'add to bag' under each image. When we click on the button 'add to bag', I want a modal to appear that ask for a confirmation. The action need to be dispatched when the user click on the confirm button inside de modal window.
The action need to grab the item object.
All is working fine ...but I'm not able to pass the item into the action when I want to launch the action from the modal.
So, my problem is not the Redux side but the modal part ( I use React-bootstrap for the modal).
I have this error message : 'item' is not defined
I'm not sure I understand exactly why this does'nt work and I failed to find a solution. I tried so many things but it's just not working.
Is there a simple way to add/pass the item data into the modal easily ?
Help would be very appreciated ! :)
Thanks a lot !!!
Here are part of my files :
Product.js
import { Modal } from "react-bootstrap";
function Products(props) {
const [show, setShow] = useState(false);
const handleClose = () => setShow(false);
const handleShow = () => setShow(true);
return (
<div>
<CardGroup>
{_.map(props.data, (item) => (
<Col>
...Some code here
<div>
<div>
{item.name.map((item) => (
<ul key={item}>{item}</ul>
))}
</div>
<div>
<button onClick={() => {handleShow()}}>
Add to bag
</button>
</div>
</div>
</Col>
))}
</CardGroup>
<Modal show={show} onHide={handleClose}>
<Modal.Body>Please confirm you want to add this product</Modal.Body>
<Modal.Footer>
<button
onClick={props.addProductToBasket(item)}
>
Confirm
</button>
<button onClick={handleClose}>Cancel</button>
</Modal.Footer>
</Modal>
</div>
);
}
const mapStateToProps = (state) => {
return { code here ...};
};
const mapDispatchToProps = (dispatch) => {
return {
addProductToBasket: (id) => dispatch(addProductToBasket(id)),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Products);
Here is the part of my Product.js file im my Store/actions folder
export const addProductToBasket = (id) => {
return {
type: ADD_PRODUCTD_TO_BASKET,
payload: id,
};
};
Since you only show the modal if an item was clicked, store the item or null instead of Boolean in the state, and open and close the modal accordingly:
function Products(props) {
const [selectedItem, setShowItem] = useState(null); // item for open model or null for closed
const handleClose = () => setShowItem(null);
const handleShow = item => setShowItem(item);
return (
<div>
<CardGroup>
{_.map(props.data, (item) => (
<Col>
...Some code here
<div>
<div>
{item.name.map((item) => (
<ul key={item}>{item}</ul>
))}
</div>
<div>
<button onClick={() => handleShow(item)}>
Add to bag
</button>
</div>
</div>
</Col>
))}
</CardGroup>
<Modal show={selectedItem !== null} onHide={handleClose}>
<Modal.Body>Please confirm you want to add this product</Modal.Body>
<Modal.Footer>
<button
onClick={() => props.addProductToBasket(selectedItem)}
>
Confirm
</button>
<button onClick={handleClose}>Cancel</button>
</Modal.Footer>
</Modal>
</div>
);
}
Not related, but it will make your life easier - use the object form of mapDispatchToProps that will save you the need to wrap with dispatch manually:
const mapDispatchToProps = {
addProductToBasket
};

React child state doesn't get updated

I have a parent component that initializes the state using hooks. I pass in the state and setState of the hook into the child, but whenever I update the state in multiple children they update the state that is not the most updated one.
To reproduce problem: when you make a link and write in your info and click submit, it successfully appends to the parent state. If you add another one after that, it also successfully appends to the parent state. But when you go back and press submit on the first link, it destroys the second link for some reason. Please try it out on my codesandbox.
Basically what I want is a button that makes a new form. In each form you can select a social media type like fb, instagram, tiktok, and also input a textfield. These data is stored in the state, and in the end when you click apply changes, I want it to get stored in my database which is firestore. Could you help me fix this? Here is a code sandbox on it.
https://codesandbox.io/s/blissful-fog-oz10p
and here is my code:
Admin.js
import React, { useState } from 'react';
import Button from '#material-ui/core/Button';
import AddNewLink from './AddNewLink';
const Admin = () => {
const [links, setLinks] = useState({});
const [newLink, setNewLink] = useState([]);
const updateLinks = (socialMedia, url) => {
setLinks({
...links,
[socialMedia]: url
})
}
const linkData = {
links,
updateLinks,
}
const applyChanges = () => {
console.log(links);
// firebase.addLinksToUser(links);
}
return (
<>
{newLink ? newLink.map(child => child) : null}
<div className="container-sm">
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
onClick={() => {
setNewLink([ ...newLink, <AddNewLink key={Math.random()} linkData={linkData} /> ])}
}
>
Add new social media
</Button>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
style={{marginTop: '50px'}}
onClick={() => applyChanges()}
>
Apply Changes
</Button>
<h3>{JSON.stringify(links, null, 4)}</h3>
</div>
</>
);
}
export default Admin;
AddNewLink.js
const AddNewLink = props => {
const [socialMedia, setSocialMedia] = useState('');
const [url, setUrl] = useState('');
const { updateLinks } = props.linkData;
const handleSubmit = () => {
updateLinks(socialMedia, url)
}
return (
<>
<FormControl style={{marginTop: '30px', marginLeft: '35px', width: '90%'}}>
<InputLabel>Select Social Media</InputLabel>
<Select
value={socialMedia}
onChange={e => {setSocialMedia(e.target.value)}}
>
<MenuItem value={'facebook'}>Facebook</MenuItem>
<MenuItem value={'instagram'}>Instagram</MenuItem>
<MenuItem value={'tiktok'}>TikTok</MenuItem>
</Select>
</FormControl>
<form noValidate autoComplete="off" style={{marginBottom: '30px', marginLeft: '35px'}}>
<TextField id="standard-basic" label="Enter link" style={{width: '95%'}} onChange={e => {setUrl(e.target.value)}}/>
</form>
<div className="container-sm">
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
style={{marginBottom: '30px'}}
onClick={() => handleSubmit()}
>
Submit
</Button>
</div>
</>
)
}
export default AddNewLink;
All I see is that links in AddNewLink would be a stale closure but in your question you never use it. Here is your code "working" since you didn't describe what it is supposed to do it always "works"
const { useState } = React;
const AddNewLink = (props) => {
const [socialMedia, setSocialMedia] = useState('');
const [url, setUrl] = useState('');
const { updateLinks, links } = props.linkData;
console.log('links is a stale closure:', links);
const handleSubmit = () => {
updateLinks(socialMedia, url);
};
return (
<div>
<select
value={socialMedia}
onChange={(e) => {
setSocialMedia(e.target.value);
}}
>
<option value="">select item</option>
<option value={'facebook'}>Facebook</option>
<option value={'instagram'}>Instagram</option>
<option value={'tiktok'}>TikTok</option>
</select>
<input
type="text"
id="standard-basic"
label="Enter link"
style={{ width: '95%' }}
onChange={(e) => {
setUrl(e.target.value);
}}
/>
<button
type="submit"
variant="contained"
color="primary"
style={{ marginBottom: '30px' }}
onClick={() => handleSubmit()}
>
Submit
</button>
</div>
);
};
const Admin = () => {
const [links, setLinks] = useState({});
const [newLink, setNewLink] = useState([]);
const updateLinks = (socialMedia, url) =>
setLinks({
...links,
[socialMedia]: url,
});
const linkData = {
links,
updateLinks,
};
const applyChanges = () => {
console.log(links);
// firebase.addLinksToUser(links);
};
return (
<React.Fragment>
{newLink ? newLink.map((child) => child) : null}
<div className="container-sm">
<button
type="submit"
variant="contained"
color="primary"
onClick={() => {
setNewLink([
...newLink,
<AddNewLink
key={Math.random()}
linkData={linkData}
/>,
]);
}}
>
Add new social media
</button>
<button
type="submit"
variant="contained"
color="primary"
style={{ marginTop: '50px' }}
onClick={() => applyChanges()}
>
Apply Changes
</button>
<h3>{JSON.stringify(links, null, 4)}</h3>
</div>
</React.Fragment>
);
};
ReactDOM.render(<Admin />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
It is not a good idea to put jsx in local state, save the data in state instead and pass that to the component every render.

how to prevent element re-rendering

I have to show a PDF file on a page and it works well but whenever I open or close the modal the PDF is re-rendered.
import { documentActions } from '../_actions'
export default function DocPreview() {
const document = useSelector(({ document }) => document)
const [show, setShow] = useState(false)
const dispatch = useDispatch()
useEffect(() => dispatch(documentActions.getDocumentContent('someDocId')), [dispatch])
return <main>
{document.loaded && <embed src={URL.createObjectURL(document.data)} type='application/pdf' />}
<button onClick={() => setShow(true)}>Sign document</button>
<Modal show={show} onHide={() => setShow(false)}>
<button className='btn btn-secondary' onClick={() => setShow(false)}>Close</button>
</Modal>
</main>
}
You can make the pdf document it's own component and only re render when document changes:
const Pdf = React.memo(function Pdf({ document }) {
document.loaded && (
<embed
src={URL.createObjectURL(document.data)}
type="application/pdf"
/>
);
});
In DocPreview
<main>
<Pdf document={document} />

Categories

Resources