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);
});
})
Related
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>
})}
</>
}
I'm trying to display the response from the API into my react component but it's not working. If I try to use it in the console, I can see the data and its value but not in the react component, it's empty when I try to show the value in a div.
Here is the code where I'm trying to display it in my react component:
const CharacterListing = () => {
const characters = useSelector(getAllCharacters);
console.log("Hello", characters);
const renderCharacters = Object.entries(characters).map(([key, value]) => {
console.log(value.name);
<div>{value.name}</div>
})
return (
<div>
{renderCharacters}
</div>
);
};
export default CharacterListing;
This is the code for my Character Slice Component
const initialState = {
characters: {},
};
const characterSlice = createSlice({
name: 'characters',
initialState,
reducers: {
addCharacters: (state, { payload }) => {
state.characters = payload;
},
},
});
export const { addCharacters } = characterSlice.actions;
export const getAllCharacters = (state) => state.characters.characters;
export default characterSlice.reducer;
This is the code for my Home Component:
const Home = () => {
const dispatch = useDispatch();
useEffect(() => {
const fetchCharacters = async () => {
const response = await baseURL.get(`/characters`)
.catch(error => {
console.log("Error", error);
});
dispatch(addCharacters(response.data));
console.log("Success", response);
};
fetchCharacters();
}, [])
return (
<div>
Home
<CharacterListing />
</div>
);
};
export default Home;
Thank you
You forgot to return item into your map func
Try this :
const renderCharacters = Object.entries(characters).map(([key, value]) => {
console.log(value.name);
return <div key={key}>{value.name}</div>
})
I am trying to save the user data when he loged in like this.
const handleLogin = () => {
firebase
.auth()
.signInWithEmailAndPassword(Email, passWord)
.then((res) => {
firebase.auth().onAuthStateChanged((userData) => {
setuserData(userData);
const jsonValue = JSON.stringify(userData);
AsyncStorage.setItem("userData", jsonValue);
console.log(userData);
});
})
.then(() => navigation.navigate("HomeScreen"))
.catch((error) => console.log(error));
};
and in the Spalch I am trying to check if the userData is in local storage or not .the problem is that it goes directly to HomeScreen even if there is No Data in Local storage
any help please
const SplashScreen = ({ navigation }) => {
const [animating, setAnimating] = useState(true);
useEffect(() => {
setTimeout(() => {
setAnimating(true);
navigation.replace(AsyncStorage.getItem("userData") ? "HomeScreen" : "Log_In");
}, 500);
},
[]);
AsyncStorage.getItem returns promise so either you need to write it with async/await or in promise
useEffect(() => {
setTimeout(async() => {
setAnimating(true);
const user = await AsyncStorage.getItem("userData")
navigation.replace(user ? "HomeScreen" : "Log_In");
}, 500);
},
[]);
Here is my solution for this, a bit long but you can try it out
const SplashScreen = ({ navigation }) => {
const [animating, setAnimating] = useState();
const [redirect, setRedirect] = useState('');
const getUserData = useCallback(async () => {
const response = await AsyncStorage.getItem('userData');
setRedirect(response ? 'HomeScreen' : 'Log_In');
},[]);
useEffect(() => {
setTimeout(() => {
getUserData();
}, 500);
}, []);
useEffect(() => {
if (redirect) {
setAnimating(true);
navigation.replace(redirect);
}
}, [redirect]);
};
I want to get my items' with a api call inside useEffect`:
export const MyComponent = () => {
// const cartContext = useCartContext();
let [items, setItems] = useState([]);
useEffect(() => {
api.get('cart?detail=true').then((res: any) => {
const result = res;
setItems(result.gifts);
}).catch(err => {
console.log(err);
})
});
return (
<div className="cart-factor-items">
{
items.map((item, index) => {
return (
<div>....</div>
but I got this error message:
Rendered more hooks than during the previous render.
I'm trying to implement a refresh button but can't get it done.
This is how my code looks like:
// ParentComponent.js
const ParentComponent = () => {
const { loading, error, data } = useItems();
return (
<ChildComponent items={data} />
);
... rest of my code that shows the data
};
// ChildComponent.js
const ChildComponent = ({ items }) => {
return (
// Logic that renders the items in <li>s
<button onClick={() => console.log('Clicking this button should refresh parent component')}
)
};
// services/useItems.js
const useItems = () => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
axios
.get(API_URL + '/counter')
.then((response) => {
setItems(response.data);
setLoading(false);
})
.catch((error) => {
setLoading(false);
setError(error.message);
});
}, []);
return { loading, error, data: counters };
}
I've tried several ways but none did the work. any helps would be truly appreciated :)
I don't think useEffect is the right mechanism here. Since it's an imperative call, nothing reactive about it, useState does the job just fine:
// ParentComponent.js
const ParentComponent = () => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const refresh = () => {
axios.get(API_URL + '/counter').then((response) => {
setItems(response.data);
setLoading(false);
}).catch((error) => {
setLoading(false);
setError(error.message);
});
};
useEffect(refresh, []);
return (
<ChildComponent items={items} refresh={refresh} />
);
// ... rest of my code that shows the data
};
// ChildComponent.js
const ChildComponent = ({ items, refresh }) => {
return (
// Logic that renders the items in <li>s
<button onClick={refresh}>
Refresh
</button>
)
};
A very simple trick is to increase an integer state, let's just call it version, which would trigger a re-render of <ParentComponent /> and if useEffect depends on version, it'll re-execute the callback, so you get the "refresh" effect.
// ParentComponent.js
const ParentComponent = () => {
const [version, setVersion] = useState(0)
// when called, add 1 to "version"
const refresh = useCallback(() => {
setVersion(s => s + 1)
}, [])
const { loading, error, data } = useItems(version);
return (
<ChildComponent items={data} refresh={refresh} />
);
};
// ChildComponent.js
const ChildComponent = ({ items, refresh }) => {
return (
// Logic that renders the items in <li>s
<button onClick={refresh} />
)
};
// services/useItems.js
const useItems = (version) => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
axios
.get(API_URL + '/counter')
.then((response) => {
setItems(response.data);
setLoading(false);
})
.catch((error) => {
setLoading(false);
setError(error.message);
});
}, [version]); // <-- depend on "version"
return { loading, error, data: counters };
}
There are couple fo small parts where you need to make changes to resolve issue.
You need to create a communication for refresh
Create a function to process any processing for refresh.
Pass this as a prop to child component
In child component, call it on necessary event, in this case click
Now since you are using hooks, you need to get it invoked.
You can add a function refreshData in your useItem hook and expose it
Call this function on click of button.
You will also have to add a flag in hooks and update useEffect to be triggered on its change
This function is necessary as setItems is only available inside hook.
Following is a working sample:
const { useState, useEffect } = React;
// ParentComponent.js
const ParentComponent = () => {
const { loading, error, data, refreshData } = useItems();
const refreshFn = () => {
refreshData()
}
return (
<ChildComponent
items={data}
onClick={refreshFn}/>
);
// ... rest of my code that shows the data
};
// ChildComponent.js
const ChildComponent = ({ items, onClick }) => {
const onClickFn = () => {
console.log('Clicking this button should refresh parent component')
if(!!onClick) {
onClick();
}
}
return (
// Logic that renders the items in <li>s
<div>
<button
onClick={ () => onClickFn() }
>Refresh</button>
<ul>
{
items.map((item) => <li key={item}>{item}</li>)
}
</ul>
</div>
)
};
// services/useItems.js
const useItems = () => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [refresh, setRefresh] = useState(false)
useEffect(() => {
if (refresh) {
setItems(Array.from({ length: 5 }, () => Math.random()));
setRefresh(false)
}
}, [ refresh ]);
return {
loading,
error,
data: items,
refreshData: () => setRefresh(true)
};
}
ReactDOM.render(<ParentComponent/>, document.querySelector('.content'))
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div class='content'></div>
As correctly commented by hackape, we need to add a check for refresh and fetch data only if its true