this is a component that retrieve data from API and generate a table. One button in a column for removing the row opens a Modal.
For avoiding in first component render to trigger api.delete request, an useRef set as false in second useEffect
Modal's Delete button return row info in deleteHandler function which successfully trigger api.delete request on confirmation, remove user in backend however table is not reloading.
Once row is removed expected result is triggering api.get request and display table now updated and without row removed.
In order to get that result I tried with const [reload, setReload] = useState(false); state which introduce another dependency to both userEffect
reload state effectively reload table data updated however it cause also that api.delete request trigger with const ida undefined. Below component script it can find useEffect with my tried.
Any hint or possible solution is appreciated
import React, { Fragment, useEffect, useState, useRef } from "react";
... other imports ...
export default function AllUsers() {
const api = useApi();
const [userData, setUserData] = useState();
const [showModal, setShowModal] = useState(false);
const navigate = useNavigate();
const [modalMessage, setModalMessage] = useState();
const [removeUserId, setRemoveUserId] = useState();
const [ida, setIda] = useState();
let effectRan = useRef(false);
const [reload, setReload] = useState(false);
useEffect(() => {
(async () => {
const response = await api.get("/admin/users");
if (response.ok) {
setUserData(response.body);
} else {
setUserData(null);
}
})();
}, [api]);
useEffect(() => {
if (effectRan.current) {
// console.log("effect run");
(async () => {
const response = await api.delete("/admin/users", {
ida: ida,
});
if (response.ok && response.status === 204) {
console.log(response);
} else {
console.log(response.body.errors);
}
})();
}
return () => (effectRan.current = true);
}, [api, ida]);
const handleEditClick = (e, rowIndex) => {
// console.log("user/" + rowIndex.username);
navigate("/user/" + rowIndex.username, [navigate]);
};
const handleRemoveClick = (e, rowIndex) => {
// console.log([rowIndex]);
setShowModal(true);
setModalMessage({
title: `Remove ${rowIndex.username}`,
message: `User's email to remove ${rowIndex.email}`,
});
setRemoveUserId(rowIndex.id);
};
const closeHandler = () => {
setShowModal(false);
};
const deleteHandler = () => {
// console.log(removeUserId);
setIda(removeUserId);
setShowModal(false);
};
// console.log(ida, idb);
return (
<Fragment>
{showModal && (
<BtModal
show={showModal}
title={modalMessage.title}
message={modalMessage.message}
handleClose={closeHandler}
onConfirm={deleteHandler}
/>
)}
<Body>
<h1>User Table</h1>
{userData === undefined ? (
<Spinner animation="border" />
) : (
<>
{userData === null ? (
<p>Could not retrieve users.</p>
) : userData.length === 0 ? (
<p>There are not users in system</p>
) : (
<UserListTable
newData={userData}
handleEditClick={handleEditClick}
handleRemoveClick={handleRemoveClick}
/>
)}
</>
)}
</Body>
</Fragment>
);
}
useEffect updated with reload state:
useEffect(() => {
(async () => {
const response = await api.get("/admin/users");
if (response.ok) {
setUserData(response.body);
effectRan.current = false;
} else {
setUserData(null);
}
})();
}, [api, reload]);
useEffect(() => {
if (effectRan.current) {
// console.log("effect run");
(async () => {
const response = await api.delete("/admin/users", {
ida: ida,
});
if (response.ok && response.status === 204) {
console.log(response);
setReload(!reload);
} else {
console.log(response.body.errors);
}
})();
}
return () => (effectRan.current = true);
}, [api, ida, reload]);
Here you modify your last edit
const deleteHandler = () => {
// console.log(removeUserId);
setIda(removeUserId);
setReload(!reload)
setShowModal(false);
};
useEffect(() => {
(async () => {
const response = await api.get("/admin/users");
if (response.ok) {
setUserData(response.body);
} else {
setUserData(null);
}
})();
}, [api]);
useEffect(() => {
effectRan.current = reload;
if (effectRan.current) {
// console.log("effect run");
(async () => {
const response = await api.delete("/admin/users", {
ida: ida,
});
if (response.ok && response.status === 204) {
console.log(response);
setReload(!reload);
} else {
console.log(response.body.errors);
}
})();
}
return () => {
effectRan.current = false;
};
}, [api, ida, reload]);
It looks like you're trying to delete an item in the table and confirm it via a prompt. I can see that you've used a lot of useEffects, which might be making things a bit complicated.
Don't worry though, I have a solution that should be much simpler. Let me show you what I mean!
const Page = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState([]);
const [deleting, setDeleting] = useState(false);
const [selectedItem, setSelectedItem] = useState(null);
const deleteHandler = async() => {
setDeleting(true);
await api.delete(selectedItem);
// Next you can either update the state itself
let tD = [...data];
const index = tD.findIndex((i) => i.id == selectedItem);
tD.splice(index, 1);
setSelectedItem(tD);
// or refetch data from the backend and update the state
const d = await api.getData();
setData(d);
setDeleting(false);
setSelectedItem(null);
};
useEffect(() => {
const fetchData = async() => {
setLoading(true);
const d = await api.getData();
setData(d);
setLoading(false);
}
fetchData();
}, [])
return loading ? <div>Loading...</div> : <>
{/*Other JSX elements*/}
{selectedItem && <div>
{/*prompt body*/}
<button onClick={() => {setSelectedItem(null)}}>Cancel</button>
<button disabled={deleting} onClick={() => {deleteHandler()}}>Delete</button>
{/*Button will be disabled when the DELETE request is sent*/}
</div>}
{data.map((row) => {
return <tr key={row.id}>
<td>...</td>
<td>...</td>
<td>
<button onClick={setSelectedItem(row.id)}>Delete</button>
</td>
</tr>
})}
</>
}
Related
I'm working on implementing a braintree payment method in my react/mui app. I've found a way that works, but it's in a class component. How can I convert this info a proper functional component?
const BraintreeDropInPaymentMethod = () => {
class Store extends React.Component {
instance;
state = {
clientToken: '<BRAIN TREE KEY>'
};
async componentDidMount() {
const response = await fetch("server.test/client_token");
const clientToken = await response.json();
this.setState({
clientToken,
});
}
async buy() {
const { nonce } = await this.instance.requestPaymentMethod();
await fetch(`server.test/purchase/${nonce}`);
}
render() {
if (!this.state.clientToken) {
return (
<div>
<h1>Loading...</h1>
</div>
);
} else {
return (
<div>
<DropIn
options={{ authorization: this.state.clientToken }}
onInstance={(instance) => (this.instance = instance)}
/>
<Button
variant='contained'
onClick={this.buy.bind(this)}
>
Create Account
</Button>
<Button
variant='outlined'
sx={{ marginLeft: 3 }}
color='warning'
onClick={(e) => handleCancelAccountCreation(e)}
href='/store-front'
>
Cancel
</Button>
</div>
);
}
}
}
const [user, setUser] = useState({})
const handleCancelAccountCreation = (event) => {
setUser({})
document.getElementById('signInBtn').hidden = false
}
return (
<Store/>
)
}
this is my attempt, but I'm coming up short on how I should handle componentDidMount(). I know how to handle useState in some situations, except for this one. Also, how can I handle the 'instance' section in a functional format? thanks.
const BraintreeDropInPaymentMethod = () => {
const [token, setToken] = useState('<BRAIN TREE KEY>')
const [user, setUser] = useState({})
const contactServer = async () => {
const res = await fetch('server.test/client_token')
const clientToken = await res.json()
console.log(clientToken)
setToken(token)
}
const buy = async () => {
const { nonce } = await this.instance.requestPaymentMethod()
await fetch(`server.test/purchase/${nonce}`)
}
const handleCancelAccountCreation = (event) => {
setUser({})
document.getElementById('signInBtn').hidden = false
}
const createAccountOptions = () => {
if (!token) {
return (
<div>
<h1>Loading...</h1>
</div>
) else {
return (
<div>
<DropIn
options={ authorization: {setToken})
onInstance={(instance) => (this.instance = instance)}
/>
<Button
variant="contained'
onClick={buy}
>
Create Account
</Button
variant='outlined'
sx={{ marginLeft: 3 }}
color='warning'
onClick={(e) => handleCancelAccountCreation(e)}
href='/store-front'
>
<Button>
Cancel
</Button>
</div>
)
}
}
}
return(
<>
<createAccountOptions/>
</>
)
}
The functional equivalent of componentDidMount() is the useEffect hook.
In this case you would change this:
async componentDidMount() {
const response = await fetch("server.test/client_token");
const clientToken = await response.json();
this.setState({
clientToken,
});
}
Into something like this:
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
const response = await fetch("server.test/client_token");
const clientToken = await response.json();
setState((old) => clientToken);
};
Using the useEffect hook with an empty array as a dependency makes the function in it only run once as the component mounts.
Why do I have undefined in selectedUser?
After all, I go through the find method through the users array and the first id of the users array should be written to selectedUser
function App() {
const [selectedUserId, setSelectedUserId] = useState(null)
const [users,setUsers] = useState([])
const selectedUser = users.find(u => u.id === selectedUserId)
console.log(users)
console.log(selectedUser)
useAsyncEffect(async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/users')
const data = await res.json()
setUsers(data)
}, [])
const onUserClick = (userId) => {
setSelectedUserId(userId)
}
return (
<div>
{ selectedUser ? <ListUsers users={users} onUserClick={onUserClick} /> : <Profile user=
{selectedUser} />
}
</div>
)
}
You can use a loading state to determine when the async function is done and only then you can set the selected user. Otherwise, it remains set to 'null' when the 'return' is rendered.
Below is pseudo code.
function App() {
const [selectedUserId, setSelectedUserId] = useState(null)
const [users,setUsers] = useState([])
const selectedUser = users.find(u => u.id === selectedUserId)
const [loading, setLoading] = useState(false)
console.log(users)
console.log(selectedUser)
useAsyncEffect(async () => {
setLoading(true)
const res = await fetch('https://jsonplaceholder.typicode.com/users')
const data = await res.json()
setUsers(data)
setLoading(false)
}, [])
const onUserClick = (userId) => {
setSelectedUserId(userId)
}
if (loading){
return "Loading..."
// you can use a nicer loader component here
}
return (
<div>
{ selectedUser ? <ListUsers users={users} onUserClick={onUserClick} /> : <Profile user=
{selectedUser} />
}
</div>
)
}
Bit of a noob to redux but this appears to be quite a difficult question! I hope someone may be able to help me :)
I have build a page where you can input a search for different types of jobs. From this, it will make a get request to my DB and get all the info on this job. As this page is multi-levelled, I want to use redux to dispatch and pass the state throughout. This will help me pass my data on the job, e.g Data Analyst, through to each component so it can use the data and populate fields.
However, this was how my input field was originally setup:
export function SearchBarComp(props) {
const [isExpanded, setExpanded] = useState(false);
const [parentRef, isClickedOutside ] = useClickOutside();
const inputRef = useRef();
const [searchQuery, setSearchQuery] = useState("");
const [isLoading, setLoading] = useState(false);
const [jobPostings, setjobPostings] = useState([]);
const [noRoles, setNoRoles] = useState(false)
const isEmpty = !jobPostings || jobPostings.length === 0;
const changeHandler = (e) => {
//prevents defaulting, autocomplete
e.preventDefault();
if(e.target.value.trim() === '') setNoRoles(false);
setSearchQuery(e.target.value);
}
const expandedContainer = () => {
setExpanded(true);
}
//LINK THE BACKEND!
const prepareSearchQuery = (query) => {
//const url = `http://localhost:5000/api/role/title?title=${query}`;
const url = `http://localhost:5000/api/role/titlerole?title=${query}`;
//replaces bad query in the url
return encodeURI(url);
}
const searchRolePosition = async () => {
if(!searchQuery || searchQuery.trim() === "")
return;
setLoading(true);
setNoRoles(false);
const URL = prepareSearchQuery(searchQuery);
const response = await axios.get(URL).catch((err) => {
console.log(err);
});
if(response) {
console.log("Response", response.data);
if(response.data && response.data === 0)
setNoRoles(true);
setjobPostings(response.data);
}
setLoading(false);
}
useDebounce(searchQuery, 500, searchRolePosition)
const collapseContainer = () => {
setExpanded(false);
setSearchQuery("");
setLoading(false);
setNoRoles(false);
if (inputRef.current) inputRef.current.value = "";
};
// console.log("Value", searchQuery)
useEffect(()=> {
if(isClickedOutside)
collapseContainer();
}, [isClickedOutside])
return (
<SearchBarContainer animate = {isExpanded ? "expanded" : "collapsed"}
variants={containerVariants} transition={containerTransition} ref={parentRef}>
<SearchInputContainer>
<SearchIconSpan>
<SearchIcon/>
</SearchIconSpan>
<SearchInput placeholder = "Search for Roles"
onFocus={expandedContainer}
ref={inputRef}
value={searchQuery}
onChange={changeHandler}
/>
<AnimatePresence>
{isExpanded && (<CloseIconSpan key="close-icon"
inital={{opacity:0, rotate: 0}}
animate={{opacity:1, rotate: 180}}
exit={{opacity:0, rotate: 0}}
transition={{duration: 0.2}}
onClick={collapseContainer}>
<CloseIcon/>
</CloseIconSpan>
)}
</AnimatePresence>
</SearchInputContainer>
{isExpanded && <LineSeperator/>}
{isExpanded && <SearchContent>
{!isLoading && isEmpty && !noRoles && (
<Typography color="gray" display="flex" flex="0.2" alignSelf="center" justifySelf="center">
Start typing to search
</Typography>
)}
{!isLoading && !isEmpty && <>
{jobPostings.map((searchRolePosition) => (
<JobSection
title={searchRolePosition.title}
//will need to do something like ----
//people = {searchRolePosition.title && searchRolePosition.title.average}
// future implementations
/>
))}
</>}
</SearchContent>}
</SearchBarContainer>
)
}
As you can see, the main thing is the 'query' this creates a backend request to my titlerole, such as getting the data on Data Analyst. This all works in my frontend right now, but I can't pass that data down to the next component etc
So i'm looking to use redux.
I've created the following slice:
import { createSlice } from "#reduxjs/toolkit";
const jobSearchSlice = createSlice({
name: "jobsearch",
initialState: {
currentRole: null,
isFetching: false,
error: false,
},
reducers: {
jobsearchStart: (state) => {
state.isFetching = true;
},
jobsearchSuccess: (state, action) => {
state.isFetching = false;
state.currentRole = action.payload;
},
jobsearchFailure: (state) => {
state.isFetching = false;
state.error = true;
},
},
});
export const { jobsearchStart, jobsearchSuccess, jobsearchFailure } = jobSearchSlice.actions;
export default jobSearchSlice.reducer;
With this, I'm also using the following apiCalls.js file:
import { jobsearchStart, jobsearchSuccess, jobsearchFailure } from "./jobsearchSlice";
import { publicRequest } from "../requestMethods";
export const roleSearchQuery = async (dispatch, jobsearch) => {
dispatch(jobsearchStart());
try{
const res = await publicRequest.get("`http://localhost:5000/api/role/titlerole?title=${query}`", jobsearch);
dispatch(jobsearchSuccess(res.data))
}catch(err){
dispatch(jobsearchFailure());
}
};
My question is as a Redux noob, how do I implement this query functionality into a redux API request? What's the way to do this properly as I begin to tranisition this to an app which uses standard state management!
I have created this custom hook to fetch data:
const useSuggestionsApi = () => {
const [data, setData] = useState({ suggestions: [] });
const [url, setUrl] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
useEffect(() => {
const fetchData = () => {
setError(false);
setLoading(true);
if(url) {
fetch(url).then((res) => {
if (res.status !== 200) {
console.error(`It seems there was an problem fetching the result. Status Code: ${res.status}`)
return;
}
res.json().then((fetchedData) => {
setData(fetchedData)
})
}).catch(() => {
setError(true)
})
setLoading(false);
};
}
fetchData();
}, [url]);
return [{ data, loading, error }, setUrl];
}
export default useSuggestionsApi;
It used used in this component to render the response (suggestions).
const SearchSuggestions = ({ query, setQuery}) => {
const [{ data }, doFetch] = useSuggestionsApi();
const { suggestions } = data;
useEffect(() => {
const encodedURI = encodeURI(`http://localhost:3000/search?q=${query}`);
doFetch(encodedURI);
}, [doFetch, query]);
return (
<div className="search-suggestions__container">
<ul className="search-suggestions__list">
{suggestions.map((suggestion) => {
return (
<li className="search-suggestions__list-item" key={uuid()}>
<span>
{suggestion.searchterm}
</span>
</li>
)
})}
</ul>
</div>
);
};
export default SearchSuggestions;
Now I would like to write some unit test for the SearchSuggestions component but I am lost on how to mock the returned data from useSuggestionApi. I tried importing useSuggestionApi as a module and then mocking the response like this but with no success:
describe('SearchSuggestions', () => {
const wrapper = shallow(<SearchSuggestions/>)
it('test if correct amount of list-item elements are rendered', () => {
jest.mock("../hooks/useSuggestionsApi", () => ({
useSuggestionsApi: () => mockResponse
}));
expect(wrapper.find('.search-suggestions__list').children()).toHaveLength(mockResponse.data.suggestions.length);
});
})
I am new to testing React components so very grateful for any input!
This works:
jest.mock('../hooks/useSuggestionsApi', () => {
return jest.fn(() => [{data: mockResponse}, jest.fn()]
)
})
describe('SearchSuggestions', () => {
const wrapper = shallow(<SearchSuggestions query="jas"/>)
it('correct amount of list-items gets rendered according to fetched data', () => {
expect(wrapper.find('.search-suggestions__list').children()).toHaveLength(mockResponse.suggestions.length);
});
})
I have created a table in which I would now like to be able to delete individual rows. The button is displayed and can be clicked, but the deleteItmen function is not called. Unfortunately I'm quite new to React.js and haven't found a solution yet.
const Tab = (props) => {
const [showContent, setShowContent] = useState([]);
const [loading, setLoading] = useState(false);
const token = getUser.getCurrentUser().token;
useEffect(() => {
LoadTable();
// eslint-disable-next-line
}, []);
const LoadTable = () => {
if (props.table === undefined)
getData("device", setShowContent, setLoading);
else getData(props.table, setShowContent, setLoading);
};
const getHeader = () => {
return <tr>{showContent[0].map((elem, index) => <th>{elem}</th>)}</tr>
}
const getBody = () => {
const bodylist = Object.values(showContent.slice(1));
const body = bodylist.map((elem, index) => ({ ...elem, button: (<button onclick={() => deleteItem(elem.id)}>Delete</button>) }))
return body.map((row, index) => <tr> {Object.values(row).map((elem, i) => <td> {elem}</td>)} </tr>)
}
const deleteItem = async (id) => {
await axios.delete('http://localhost:3000/api/' + id, { tablename: props.table }, {
headers: {
"x-access-token": token,
},
})
LoadTable();
}
return (
<div>
{!loading ?
<h1>Loading</h1> :
<table>
<thead>{getHeader()}</thead>
<tbody>{getBody()}</tbody>
</table>
}
</div>
);