React TypeScript button with loading spinner - javascript

I have a button inside a react component, the test on the button says Join Now when the button is clicked I call the function submitForm which then sets the state to true. I want the loading svg image to be displayed but instead the path to the image is shown on the button.
import React, { useState } from 'react';
import Context from './context';
import loading from '../images/loading.svg';
const ButtonSpinner: React.FC = () => {
const [state, setState] = useState({loading: false});
function submitForm() {
setState({ ...state, loading: true})
}
return (
<button className="btn-join" onClick={submitForm}>
{!state.loading && 'Join Now!'}
{state.loading && loading}
</button>
);
}
export {ButtonSpinner};

try displaying image like this instead- you need an img tag in order to display image content, React won't do that out of the box.
return (
<button className="btn-join" onClick={submitForm}>
{!state.loading && 'Join Now!'}
{state.loading && <img src={loading} /> }
</button>
);

import React, { useState } from 'react';
import Context from './context';
import loading from '../images/loading.svg';
const ButtonSpinner: React.FC = () => {
const [state, setState] = useState({loading: false});
function submitForm() {
setState({ ...state, loading: true})
}
return (
<button className="btn-join" onClick={submitForm}>
{!state.loading ? 'Join Now!' : <img src={loading} alt="loading" />}
</button>
);
}
export {ButtonSpinner};

Related

Why useContext doesn't re-render the component - React.js + Laravel

I'm stucked... :)
I have a single view in Laravel where React appends to the specific ID. What I try to do is to open the pop-up after click the button using an useContext. Below is my code:
Globalcontext.js
import React from 'react';
export const initialState = {
usersData: null,
clickedNewUserButton: false,
showAddUserPopup: false,
isLoading: true,
};
export const GlobalContext = React.createContext(initialState);
UsersPageMain.js
import React, { useEffect, useContext } from 'react';
import ReactDOM from 'react-dom';
import { GlobalContext } from '../state/GlobalContext';
import FiltersButton from '../components/Users/FiltersButton';
import AddUserButton from '../components/Users/AddUserButton';
import UsersTable from '../components/Users/UsersTable';
import AddNewUserPopup from '../components/Users/AddNewUserPopup';
function UsersPageMain(){
const initialState = useContext(GlobalContext);
if(initialState.clickedNewUserButton){
return (
<GlobalContext.Provider value={initialState}>
<div className='container users-list-page'>
<div className='row'>
<FiltersButton/>
<AddUserButton/>
</div>
<div className='row'>
<UsersTable></UsersTable>
</div>
</div>
<AddNewUserPopup/>
</GlobalContext.Provider>
)
}else{
return (
<GlobalContext.Provider value={initialState}>
<div className='container users-list-page'>
<div className='row'>
<FiltersButton/>
<AddUserButton/>
</div>
<div className='row'>
<UsersTable></UsersTable>
</div>
</div>
</GlobalContext.Provider>
)
}
}
export default UsersPageMain;
if (document.getElementById('user-list-page')) {
ReactDOM.render(<UsersPageMain />, document.getElementById('user-list-page'));
}
UsersTable.js
import axios from 'axios';
import React, {useContext,useEffect,useState} from "react";
import { GlobalContext } from "../../state/GlobalContext";
import Preloader from '../Preloader';
import Conf from '../../conf/Conf';
export default function UsersTable(){
const context = useContext(GlobalContext);
const [loading, setLoading] = useState(true);
useEffect(() => {
axios.get('/api/get-all-users')
.then(response => {
context.usersData = response.data.data;
setLoading(false);
})
})
if(loading){
return (
<Preloader isLoading={loading}/>
)
}else{
return (
<>
<Preloader isLoading={loading}/>
<div className="col-12">
<div className="table-responsive rounded-table">
<table className="table">
<thead>
<tr>
<th>ID</th>
<th>Avatar</th>
<th>Name</th>
<th>Surname</th>
<th>Group</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{context.usersData.map((user,index) => {
return (
<tr key={user.email}>
<th>{user.id}</th>
<th>
<div className="avatar" style={{backgroundImage: `url(${Conf().assetPath}uploads/avatars/${user.avatar})`}}></div>
</th>
<th>{user.name}</th>
<th>{user.surname}</th>
<th>Group</th>
<th>{user.email}</th>
<th>
<button type="button" className="btn theme-edit-btn"><i className="fa-solid fa-pencil"></i></button>
<button type="button" className="btn theme-delete-btn"><i className="fa-solid fa-delete-left"></i></button>
</th>
</tr>
)
})}
</tbody>
</table>
</div>
</div>
</>
)
}
}
AddUserButton.js
import React, { useContext } from "react";
import { GlobalContext } from "../../state/GlobalContext";
export default function AddUserButton(){
const context = useContext(GlobalContext);
function triggerUserPopupClick(){
context.clickedNewUserButton = true
}
return(
<div className="col-xl-2 col-md-4 col-12">
<button type="button" onClick={() => {triggerUserPopupClick()}} className="btn theme-primary-btn">Add New <i className="fa-solid fa-angles-right"></i></button>
</div>
)
}
The problem is exactly in the AddUserButton component where I'm trying to update global context to re-render main UsersPageMain component. I don't have any idea what I'm doing wrong... If you could, please give some tips what I need to do to achieve opening popup after clicking this component.
Here's the working snippet on the codesanbox, might it will be helpfull.
https://codesandbox.io/embed/adoring-bell-dv3tfc?fontsize=14&hidenavigation=1&theme=dark
function triggerUserPopupClick(){
context.clickedNewUserButton = true
}
React has only one1 way to cause a rerender: setting state. React can't tell that you mutated the context object here, so it will not rerender.
To make this work, you will need to set up your top level component to have a state variable. It will then make both the state and the setter function available via context.
// In globalcontext.js
export const initialState = {
usersData: null,
clickedNewUserButton: false,
showAddUserPopup: false,
isLoading: true,
};
export const GlobalContext = React.createContext({
state: initialState,
setState: () => {},
});
// In UsersPageMain.js
function UsersPageMain(){
const [state, setState] = useState(initialState);
// It's important to memoize this so you don't make a new object unless
// the state has changed. Otherwise, every render of UsersPageMain will
// necessarily trigger a render of every consumer of the context.
const contextValue = useMemo(() => {
return {
state,
setState,
};
}, [state]);
if(state.clickedNewUserButton){
return (
<GlobalContext.Provider value={contextValue}>
// ...
</GlobalContext.Provider>
);
} else {
return (
<GlobalContext.Provider value={contextValue}>
// ...
</GlobalContext.Provider>
)
}
}
And then you'll call that setState function when you want to change it:
// In AddUserButtonjs
const { state, setState } = useContext(GlobalContext);
function triggerUserPopupClick(){
setState({
...state,
clickedNewUserButton: true
});
}
Don't forget to update any other places where you're using the context so that they expect to get an object with a .state property, instead of getting the state directly.
1) Ok, technically there's a second way: force update in class components. But don't use that
I figured out what was wrong. I wrote the async function and then into the useEffect I resolve the promise. The problem is that still I don't know why my axios was getting into the loop. Here's a working part of code :
import axios from 'axios';
import React, {useContext,useEffect,useState} from "react";
import { GlobalContext } from "../../state/GlobalContext";
import Preloader from '../Preloader';
import Conf from '../../conf/Conf';
async function fetchUsersData() {
const response = await axios.get('/api/get-all-users');
return response;
}
export default function UsersTable(){
const { state, setState } = useContext(GlobalContext);
useEffect( () => {
fetchUsersData().then(response => {
setState({
...state,
usersData: response.data.data,
isLoading: false,
})
})
}, [null]);
return (
<></>
)
}

setState is not a function in react native

I am learning react native, have been getting this error setState is not a function in react native
I searched a lot but nothing was helpful enough.
I have created this simplified code to show the issue
import React, { useState } from "react";
import { Text, View, Button } from "react-native";
const Test = ({ Test1 }) => {
return (
<Button
onPress={() => {
Test1.setState(true);
}}
/>
);
};
const Test1 = () => {
const [state, setState] = useState(false);
if (state) {
return <Text>Test Working</Text>;
} else {
return <Text>Test Not Working</Text>;
}
};
const App = () => {
return (
<View>
<Test Test1={Test1} />
</View>
);
};
export default App;
this is the error: TypeError: Test1.setState is not a function
Please help me fix this.
States can be transferred to other component only as props. You need to call the Test1 component from the App and the Test component from the Test1, then you can pass the props to the Test from Test1. By this you don't need to move the state to other component. you can not pass any component as props and access state or methods from there. You can try this code:
import React, { useState } from "react";
import { Text, View, Button } from "react-native";
const Test = ({ setState}) => {
return (
<Button
onPress={() => {
setState(true);
}}
/>
);
};
const Test1 = () => {
const [state, setState] = useState(false);
if (state) {
return <Text>Test Working</Text>;
} else {
return <Test setState={setState} />;
}
};
const App = () => {
return (
<View>
<Test1 />
</View>
);
};
export default App;
import React, { useState } from "react";
import { Text, View, Button } from "react-native";
const Test = ({ setState }) => {
return (
<Button
onPress={() => {
setState(true);
}}
);
};
const Test1 = ({state}) => {
if (state) {
return <Text>Test Working</Text>;
} else {
return <Text>Test Not Working</Text>;
}
};
const App = () => {
const [state, setState] = useState(false);
return (
<View>
<Test1 state={state} />
<Test setState={setState} />
</View>
);
};
export default App;
There are two problems here.
Your Test1 component is not being used at all
Hooks, and local functions in general, may not be called outside of the component that they are declared on
If you want to manage some local state in your Test component, it needs to live in that component.

Why is a function passed through props into a child component behaving differently on a key press vs. button click?

I have a React project where the parent component (functional) holds state to determine what formatting is applied to a list. When the user clicks a button, a modal is generated - I pass an anonymous function to that modal as props (onMainButtonClick), which when called flips the state and changes the formatting of the list (logic for this is in parent component).
When I use the button in my modal component (onClick={() => onMainButtonClick()}), the code works as expected. However, I would also like an enter press to trigger this. Therefore I have the following code implemented for this, but it doesn't function as expected. The modal closes and the function fires (I know this as I put a console log in there...) but the state that impacts the formatting is not changed.
EDIT: Having made a proposed change below (to memo'ize the onEnterPress function so it gets removed properly), here's the full code for the modal:
import { React, useRef, useEffect, useCallback } from "react";
import ReactDOM from "react-dom";
const Modal = ({
title,
description,
isOpen,
onClose,
onMainButtonClick,
mainButtonText,
secondaryButtonText,
closeOnly,
}) => {
const node = useRef();
const onEnterPress = useCallback(
(e) => {
if (e.key === "Enter") {
onMainButtonClick();
}
},
[onMainButtonClick]
);
useEffect(() => {
window.addEventListener("keydown", onEnterPress);
return () => {
window.removeEventListener("keydown", onEnterPress);
};
}, [onMainButtonClick]);
const handleClickOutside = (e) => {
if (node.current.contains(e.target)) {
return null;
}
onClose();
};
if (!isOpen) return null;
return ReactDOM.createPortal(
{// JSX here, removed for brevity},
document.body
);
};
export default Modal;
And the code for the parent (which shows the modal and passes down onMainButtonClick):
import { React, useState } from "react";
import { decode } from "html-entities";
import useList from "../../queries/useList";
import useBuyItem from "../../mutations/useBuyItem";
import Header from "../Shared/Header";
import Description from "../Shared/Description";
import ListItem from "./ListItem";
import BuyOverlay from "./BuyOverlay";
import Modal from "../Modal";
import ViewOperations from "./ViewOperations";
const View = (props) => {
const [buyOverlayOpen, setBuyOverlayOpen] = useState(false);
const [viewBuyersOverlayOpen, setBuyersOverlayOpen] = useState(false);
const [viewBuyers, setViewBuyers] = useState(false);
const [buyItemId, setBuyItemId] = useState();
const { isLoading, isError, data, error } = useList(props.match.params.id);
const buyItemMutation = useBuyItem(buyItemId, props.match.params.id, () =>
setBuyOverlayOpen(false)
);
if (isLoading) {
return <div>Loading the list...</div>;
}
if (isError) {
return <div>An error occured, please refresh and try again</div>;
}
return (
<div className="w-full">
<div className="mb-10 text-gray-600 font-light">
<Header text={data.name} />
<Description text={data.description} />
<ViewOperations
toggleViewBuyers={() => setViewBuyers(!viewBuyers)}
toggleBuyersOverlay={() =>
setBuyersOverlayOpen(!viewBuyersOverlayOpen)
}
viewBuyers={viewBuyers}
/>
<div id="list" className="container mt-10">
{data.items.length === 0
? "No items have been added to this list"
: ""}
<ul className="flex flex-col w-full text-white md:text-xl">
{data.items.map((item) => {
return (
<ListItem
item={decode(item.item)}
description={decode(item.description)}
itemId={item._id}
isBought={decode(item.bought)}
boughtBy={decode(item.boughtBy)}
boughtDate={decode(item.boughtDate)}
viewlink={item.link}
handleBuy={() => {
setBuyItemId(item._id);
setBuyOverlayOpen(true);
}}
key={item._id}
viewBuyers={viewBuyers}
/>
);
})}
</ul>
</div>
</div>
<BuyOverlay
isOpen={buyOverlayOpen}
message="Message"
onClose={() => setBuyOverlayOpen(false)}
onConfirmClick={(buyerName) => {
buyItemMutation.mutate(buyerName);
}}
/>
<Modal
title="Title"
description="Description"
isOpen={viewBuyersOverlayOpen}
onClose={() => {
setBuyersOverlayOpen(false);
}}
onMainButtonClick={() => {
setViewBuyers(true);
setBuyersOverlayOpen(false);
}}
mainButtonText="OK"
secondaryButtonText="Cancel"
/>
</div>
);
};
export default View;
Any ideas for why this is happening? I have a feeling it might be something to do with useEffect here, but I'm a bit lost otherwise...
onEnterPress is most probably directly defined in your functional component and that's why its reference changes every time your component re-renders. Your useEffect closes over this newly defined function on each render so your handler is not going to work as you expected.
You can wrap your onEnterPress with useCallback to memoize your handler onEnterPress so its definition stays the same throughout each render just like this:
const onEnterPress = useCallback(e => {
if (e.key === "Enter") {
onMainButtonClick();
}
}, [onMainButtonClick]); // Another function dep. You can also wrap it with useCallback or carry over its logic into the callback here
useEffect(() => {
window.addEventListener('keydown', onEnterPress);
return () => {
window.removeEventListener('keydown', onEnterPress);
};
}, [onEnterPress]);
I figured this out in the end - turns out it was as simple as my enterPress callback needing a preventDefault statement before the if block (e.preventDefault()).
There must have been something else on the page that was capturing the enter press and causing it to behave strangely.

How to pass id from one component to another component onclick of an element

I'm trying to pass this is as id as props to another component which is not a child of the component. I was considering using context but i wanted to know if there was another way to it, since I'm quite new to react I'm looking for a more efficient way.
This is the component where the id of the element clicked is being generated. When i logged it the data is correct an no problems was notified. I first tried passing it as props as seen below but since i didn't want it to be seen on that page i didn't pass it to the main return statement neither did i call the method in it, but then it returned undefined in the component where i wanted to make use of it
import React, { useState } from 'react'
import { useHistory } from 'react-router-dom';
import Workspacelist from '../Workspace/Workspacelist';
function BoardList({ boards }) {
const [currentid, setcurrentid] = useState('')
const history = useHistory()
const navigate = (id) => {
setcurrentid(id);
console.log(id)
history.push(`/workspace/${id}`)
return(
<Workspacelist id = {id}/>
)
}
return (
<>
{
boards.map((board) => (
<li key={board.id} className="boardlist" style={styles} onClick={() => navigate(board.id)}>
<h3>{board.title}</h3>
</li>
))}
</>
)
}
export default BoardList
PS: Firebase is being incoporated in this project, i was thinking that might be the reason cause it's my first time using firebase so maybe I'm missing something since all the data is coming from the server
And this is the component i want to pass it to
import React, { useState, useEffect } from 'react'
import Firebase, { db } from '../Firebase/Firebase';
import { Todo } from './List';
function Workspacelist({ id }) {
const [updatedId] = useState(id)
const [show, setshow] = useState(false);
const [Todos, setTodos] = useState([]);//Todolist
const [ToDo, setToDo] = useState('');
useEffect(() => {
const docRef = db.collection("boards").doc(updatedId).get().then(doc => {
if (doc.exists) {
setTodos(doc.data().todo);
console.log("Document data:", doc.data().todo);
} else {
console.log("No such document!");
}
}).catch(function (error) {
console.log("Error getting document:", error);
});
return docRef
})
return (
<div className="workspacelist">
<div className="todo">
<div>
<b>To Do</b>
<b>...</b>
<Todo Todos={Todos} />
<span onClick={() => { setshow(current => !current) }} style={{ display: show ? 'none' : 'block' }}>+ Add a card</span>
</div>
<div className="add" style={{ display: show ? 'block' : 'none' }}>
<textarea placeholder="Enter a title for this card..." value={ToDo} onChange={(e) => { setToDo(e.target.value) }} />
<button className="addcard" onClick={one}>Add Card</button>
<button onClick={() => { setshow(current => !current) }}>X</button>
<button className="more">...</button>
</div>
</div>
</div>
)
}
export default Workspacelist
Thanks in advance i did appreciate the help even if i have to rewrite it just tell me the way you would do it if you were in my shoes
To navigate to another page, you just need history.push(/workspace/${id}).
You don't even need any state here.
import React, { useState } from 'react'
import { useHistory } from 'react-router-dom';
import Workspacelist from '../Workspace/Workspacelist';
function BoardList({ boards }) {
const history = useHistory()
const navigate = (id) => {
history.push(`/workspace/${id}`)
}
return (
<>
{
boards.map((board) => (
<li key={board.id} className="boardlist" style={styles} onClick={() => navigate(board.id)}>
<h3>{board.title}</h3>
</li>
))}
</>
)
}
export default BoardList
To get the id param on the Workspace page, you will need to use the useRouteMatch hook from react-router-dom:
import { useRouteMatch } from 'react-router-dom';
function Workspacelist() {
const {
params: { id },
} = useRouteMatch('/workspace/:id');
console.log(id)
}
Let me know if it solves your problem.
If you use dom version 6, change the following parts that showed in #HichamELBSI answer.
useHistory should change into useNavigate.
useRouteMatch should change into useMatch.
After applying those, the codes should be
import { useNavigate} from 'react-router-dom';
const nav = useNavigate();
const navigate = (id) => {
nav(`/workspace/${id}`)
}
Then other part should be
import { useMatch } from 'react-router-dom';
function Workspacelist() {
const {
params: { id },
} = useMatch('/workspace/:id');
console.log(id)
}

Unable to trigger function from context api with react hooks

I'm trying to trigger a function from my CartContext Api upon a click, but it isn't happening. I have checked the method and it works, but when I add the context function it doesn't do anything... see below code:
Context file
import React, { useState } from 'react';
export const CartContext = React.createContext({
cart: [],
setCart: () => {},
});
const CartContextProvider = (props) => {
const [updateCart, setUdatedCart] = useState();
const updateCartHandler = () => {
console.log('click');
};
return (
<CartContext.Provider
value={{ cart: updateCart, setCart: updateCartHandler }}
>
{props.children}
</CartContext.Provider>
);
};
export default CartContextProvider;
Component where Im using the context:
import React, { useContext } from 'react';
import classes from './SingleProduct.css';
import AddToCartBtn from './AddToCartBtn/AddtoCartBtn';
import { CartContext } from '../context/cart-context';
const singleProduct = (props) => {
const cartContext = useContext(CartContext);
const addToCart = (id, productName, price, qty) => {
const productInCart = {
productId: id,
productName: productName,
productPrice: price,
productQty: qty,
};
cartContext.setCart();
};
return (
<article className={classes.SingleProduct}>
<div className={classes.ProductImgContainer}>
<img src={props.productImg} alt="" />
</div>
<div className={classes.ProductTitle}>
<h2>{props.productName}</h2>
</div>
<AddToCartBtn
clicked={() => {
addToCart(
props.productId,
props.productName,
props.productPrice,
props.productQty
);
}}
/>
</article>
);
};
export default singleProduct;
I'm just adding a console.log('click') to check if the method triggers at the moment. By the way, when I console.log the context variable it contains the properties and works. Any ideas why this isn't happening
Forgot to wrap the component with provider thanks!

Categories

Resources