I want to use react-firebase-hooks for firestore, but I cant seem to make it work properly, I have collection of users and it has subcollection of budgets. If user is logged in I want to show his budgets. My code:
The problem is mainly in useCollection hook, where I need to pass user.uid, but user is undefined in the beginning. How else should I do this?
const Homepage = () => {
const [user, loading, error] = useAuthState(auth);
const [showAddBudgetModal, setShowAddBudgetModal] = useState(false);
const [value, dataLoading, dataError] = useCollection(collection(db, `users/${user!.uid}/budgets`), {
snapshotListenOptions: { includeMetadataChanges: true },
});
const [showAddExpenseModal, setShowAddExpenseModal] = useState(false);
const [addExpenseModalBudgetId, setAddExpenseModalBudgetId] = useState('');
const openAddExpenseModal = (budgetId?: any) => {
setShowAddExpenseModal(true);
setAddExpenseModalBudgetId(budgetId);
};
if (loading) {
return (
<div>
<p>Initialising User...</p>
</div>
);
}
if (error) {
return (
<div>
<p>Error: {error.message}</p>
</div>
);
}
if (user) {
return (
<div>
<h1>Welcome {user.displayName}!</h1>
<button onClick={logOut}>Logout</button>
<div className="buttons">
<button onClick={() => setShowAddBudgetModal(true)}>Add budget</button>
</div>
<AddBudgetModal show={showAddBudgetModal} onClose={() => setShowAddBudgetModal(false)}></AddBudgetModal>
<div>
{dataError && <strong>Error: {JSON.stringify(error)}</strong>}
{dataLoading && <span>Collection: Loading...</span>}
{value && user && (
<div>
{value.docs.map((budget) => (
<BudgetCard
key={budget.id}
name={budget.name}
max={budget.max}
onAddExpenseClick={() => openAddExpenseModal(budget.id)}
onViewExpensesClick={() => openAddExpenseModal(budget.id)}
></BudgetCard>
))}
</div>
)}
</div>
</div>
);
}
};
export default Homepage;
There are 2 ways to solve this problem in my opinion
First one is to use optional chaining to check whether the user exist or not eg:
const [value, dataLoading, dataError] = useCollection(user && query(
collection(getFirestore(app), "users", user.uid, "budgets"), {
snapshotListenOptions: {
includeMetadataChanges: true
},
});
For more information about this there is a similar thread about this issue.
Second way is to use useEffect hook to implement the same like this:
useEffect(() => {
if (user) {
const [value, dataLoading, dataError] = useCollection(user && query(
collection(getFirestore(app), "users", user.uid, "budgets"), {
snapshotListenOptions: {
includeMetadataChanges: true
},
});
}
}, [user]);
In this way your user can only be rendered if and only if data related to the user is loaded already, but make sure in future to not set user in useEffect as it will create an infinite loop as useEffect has provided user in dependencies array.
For more about usEffect you can go through this documentations
Related
I want to know how improve this calls in order to not repeat always the same sentence to refresh the state...
I don't need any huge refactor, only inputs like: you need to put this call inside a function and call it when you want... something like this...
export const CategoriesPage = () => {
const [categories, setCategories] = useState<Category[]>([]);
const [showModal, setShowModal] = useState(false);
const handleCreateCategory = (newCategory: CategoryCreate, file: File) => {
createCategoryHelper(newCategory, file)
.then(() => {
getCategoriesHelper().then(setCategories);
})
.finally(() => handleClose());
};
const handleDeleteCategory = (categoryId: Id) => {
SwalHelper.delete().then(() => {
deleteCategoryHelper(categoryId).then(() =>
getCategoriesHelper().then(setCategories)
);
});
};
const handleClose = () => {
setShowModal(false);
};
const handleModal = () => {
setShowModal(true);
};
useEffect(() => {
getCategoriesHelper().then(setCategories);
}, []);
return (
<>
<PageTitle title="Categories" />
<FilterBar>
<Button type="button" background="green" onClick={handleModal}>
+ Add new
</Button>
</FilterBar>
{showModal && (
<ModalPortal onClose={handleClose}>
<CreateCategoryForm
createCategory={(category, file: File) => {
handleCreateCategory(category, file);
}}
/>
</ModalPortal>
)}
<ListGrid columns={3}>
{categories.map((category) => {
const { id: categoryId } = category;
return (
<CategoryCard
key={categoryId}
{...category}
onClick={() => handleDeleteCategory(categoryId)}
/>
);
})}
</ListGrid>
</>
);
};
When component is mounting, on useEffect, fills the state with response in order to create a list.
When a category is created, I call to setState again to refresh the list.
Same on delete, on then, refresh again to update the list.
Three times calling the same sentence
getCategoriesHelper().then(setCategories)
This is getCategoriesHelper:
export const getCategoriesHelper = async () => {
const service = new CategoryServiceImplementation(apiConfig);
const uploadImageService = new AmplifyS3Service();
const repository = new CategoryRepositoryImplementation(
service,
uploadImageService
);
const useCase = new GetCategoriesUseCaseImplementation(repository);
return await useCase.getCategories();
};
Is there any way to make this code much cleaner and reusable?
Thanks in advance!
Everything is write, and all calls are made as they are designed to do
Note: I've seen people suggesting useEffect to this issue but I am not updating the state through useEffect here..
The problem I am having is that when a user selects id 7 for example, it triggers a function in App.tsx and filters the todo list data and update the state with the filtered list. But in the browser, it doesn't reflect the updated state immediately. It renders one step behind.
Here is a Demo
How do I fix this issue (without combining App.tsx and TodoSelect.tsx) ?
function App() {
const [todoData, setTodoData] = useState<Todo[]>([]);
const [filteredTodoList, setFilteredTodoList] = useState<Todo[]>([]);
const [selectedTodoUser, setSelectedTodoUser] = useState<string | null>(null);
const filterTodos = () => {
let filteredTodos = todoData.filter(
(todo) => todo.userId.toString() === selectedTodoUser
);
setFilteredTodoList(filteredTodos);
};
useEffect(() => {
const getTodoData = async () => {
console.log("useeffect");
try {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/todos"
);
setTodoData(response.data);
setFilteredTodoList(response.data);
} catch (error) {
console.log(error);
}
};
getTodoData();
}, []);
const handleSelect = (todoUser: string) => {
setSelectedTodoUser(todoUser);
filterTodos();
};
return (
<div className="main">
<TodoSelect onSelect={handleSelect} />
<h1>Todo List</h1>
<div>
{" "}
{filteredTodoList.map((todo) => (
<div>
<div>User: {todo.userId}</div>
<div>Title: {todo.title}</div>
</div>
))}
</div>
</div>
);
}
In TodoSelect.tsx
export default function TodoSelect({ onSelect }: TodoUsers) {
const users = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"];
return (
<div>
<span>User: </span>
<select
onChange={(e) => {
onSelect(e.target.value);
}}
>
{users.map((item) => (
<option value={item} key={item}>
{item}
</option>
))}
</select>
</div>
);
}
There's actually no need at all for the filteredTodoList since it is easily derived from the todoData state and the selectedTodoUser state. Derived state doesn't belong in state.
See Identify the Minimal but Complete Representation of UI State
Let’s go through each one and figure out which one is state. Ask three
questions about each piece of data:
Is it passed in from a parent via props? If so, it probably isn’t state.
Does it remain unchanged over time? If so, it probably isn’t state.
Can you compute it based on any other state or props in your component? If so, it isn’t state.
Filter the todoData inline when rendering state out to the UI. Don't forget to add a React key to the mapped todos. I'm assuming each todo object has an id property, but use any unique property in your data set.
Example:
function App() {
const [todoData, setTodoData] = useState<Todo[]>([]);
const [selectedTodoUser, setSelectedTodoUser] = useState<string | null>(null);
useEffect(() => {
const getTodoData = async () => {
console.log("useeffect");
try {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/todos"
);
setTodoData(response.data);
} catch (error) {
console.log(error);
}
};
getTodoData();
}, []);
const handleSelect = (todoUser: string) => {
setSelectedTodoUser(todoUser);
};
return (
<div className="main">
<TodoSelect onSelect={handleSelect} />
<h1>Todo List</h1>
<div>
{filteredTodoList
.filter((todo) => todo.userId.toString() === selectedTodoUser)
.map((todo) => (
<div key={todo.id}>
<div>User: {todo.userId}</div>
<div>Title: {todo.title}</div>
</div>
))
}
</div>
</div>
);
}
State update doesn't happen synchronously! So, selectedTodoUser inside filterTodos function is not what you're expecting it to be because the state hasn't updated yet.
Make the following changes:
Pass todoUser to filterTodos:
const handleSelect = (todoUser: string) => {
setSelectedTodoUser(todoUser);
filterTodos(todoUser);
};
And then inside filterTodos compare using the passed argument and not with the state.
const filterTodos = (todoUser) => {
let filteredTodos = todoData.filter(
(todo) => todo.userId.toString() === todoUser
);
setFilteredTodoList(filteredTodos);
};
You probably won't need the selectedTodoUser state anymore!
I changed option but useeffect not update input. Please guide me where i make mistake. First i use useEffect to setCurrency after that i use mapping for getCurrency to add it on Select option. onCitySelect i added it to setSelectedId when i change Select option. Lastly, i tried to get address with api but the problem is i need to change api/address?currency=${selectId.id}` i added selectedId.id but everytime i change option select it is not affect and update with useEffect. I tried different solution couldn't do it. How can i update useEffect eveytime option select change (selectId.id) ?
export default function Golum() {
const router = useRouter();
const dispatch = useDispatch();
const [getCurrency, setCurrency] = useState("");
const [getAddress, setAddress] = useState("");
const [selectCity, setSelectCity] = useState("");
const [selectId, setSelectId] = useState({
id: null,
name: null,
min_deposit_amount: null,
});
const [cityOptions, setCityOptions] = useState([]);
useEffect(() => {
setSelectCity({ label: "Select City", value: null });
setCityOptions({ selectableTokens });
}, []);
const onCitySelect = (e) => {
if (e == null) {
setSelectId({
...selectId,
id: null,
name: null,
min_deposit_amount: null,
});
} else {
setSelectId({
...selectId,
id: e.value.id,
name: e.value.name,
min_deposit_amount: e.value.min_deposit_amount,
});
}
setSelectCity(e);
};
const selectableTokens =
getCurrency &&
getCurrency.map((value, key) => {
return {
value: value,
label: (
<div>
<img
src={`https://central-1.amazonaws.com/assets/icons/icon-${value.id}.png`}
height={20}
className="mr-3"
alt={key}
/>
<span className="mr-3 text-uppercase">{value.id}</span>
<span className="currency-name text-uppercase">
<span>{value.name}</span>
</span>
</div>
),
};
});
useEffect(() => {
const api = new Api();
let mounted = true;
if (!localStorage.getItem("ACCESS_TOKEN")) {
router.push("/login");
}
if (mounted && localStorage.getItem("ACCESS_TOKEN")) {
api
.getRequest(
`${process.env.NEXT_PUBLIC_URL}api/currencies`
)
.then((response) => {
const data = response.data;
dispatch(setUserData({ ...data }));
setCurrency(data);
});
}
return () => (mounted = false);
}, []);
useEffect(() => {
const api = new Api();
let mounted = true;
if (!localStorage.getItem("ACCESS_TOKEN")) {
router.push("/login");
}
if (mounted && localStorage.getItem("ACCESS_TOKEN")) {
api
.getRequest(
`${process.env.NEXT_PUBLIC_URL}api/address?currency=${selectId.id}`
)
.then((response) => {
const data = response.data;
dispatch(setUserData({ ...data }));
setAddress(data.address);
})
.catch((error) => {});
}
return () => (mounted = false);
}, []);
return (
<div className="row mt-4">
<Select
isClearable
isSearchable
onChange={onCitySelect}
value={selectCity}
options={selectableTokens}
placeholder="Select Coin"
className="col-md-4 selectCurrencyDeposit"
/>
</div>
<div className="row mt-4">
<div className="col-md-4">
<Form>
<Form.Group className="mb-3" controlId="Form.ControlTextarea">
<Form.Control className="addressInput" readOnly defaultValue={getAddress || "No Address"} />
</Form.Group>
</Form>
</div>
</div>
);
}
The second parameter of the useEffect hook is the dependency array. Here you need to specify all the values that can change over time. In case one of the values change, the useEffect hook re-runs.
Since you specified an empty dependency array, the hook only runs on the initial render of the component.
If you want the useEffect hook to re-run in case the selectId.id changes, specify it in the dependency array like this:
useEffect(() => { /* API call */ }, [selectId.id]);
I think you are accessing the e object wrong. e represents the click event and you should access the value with this line
e.target.value.id
e.target.value.value
My application has a user input an id to send as a request and the response data that matches that id is rendered and also cached.
I'd like to access the cache so that I can also display its data so that a user can see their previous queries. However, I receive this error with my attempts. TypeError: Cannot read property 'getQueryData' of undefined.
The following are my attempts to access the cache.
console.log(queryCache.getQueryData("rickandmorty"))
console.log(queryCache.getQueryData(["rickandmorty", idQuery]))
console.log(queryClient.getQueryData("rickandmorty"))
console.log(queryClient.getQueryData(["rickandmorty", idQuery]))
Please let me know what I'm doing wrong. I'm using version "react-query": "^3.13.0" https://codesandbox.io/s/rick-and-morty-2gy8r?file=/src/App.js
const [idQuery, setIdQuery] = useState(0);
const [rickAndMortyCharacter, setRickAndMortyCharacter] = useState({});
const queryRef = useRef(null);
const { register, handleSubmit, errors, setValue, watch } = useForm({
resolver: yupResolver(searchSchema)
});
const formId = watch("rickAndMortyId");
const handleRickAndMortyFetch = () => {
return delay()
.then(() => axios(`https://rickandmortyapi.com/api/character/${idQuery}`))
.then((res) => setRickAndMortyCharacter(res.data));
};
const { isLoading, error } = useQuery(
["rickandmorty", idQuery],
handleRickAndMortyFetch,
{
refetchOnWindowFocus: false,
enabled: idQuery !== 0,
useErrorBoundary: true
}
);
const disable = isLoading || parseFloat(formId) === idQuery;
const onSubmit = (formData) => {
setIdQuery(formData.rickAndMortyId);
};
return (
<div>
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
queryRef.current.focus();
}}
resetKeys={[idQuery]}
>
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<h3>Rick and Morty</h3>
<input
type="number"
name="rickAndMortyId"
placeholder="Between 1 and 671"
ref={register}
disabled={isLoading}
/>
{errors.rickAndMortyId && <span>This field is required</span>}
{error && <p>Error occurred: {error.message}</p>}
<button type="submit" disabled={disable}>
Search Character
</button>
</form>
<button
onClick={() => {
const randomId = getRandomRickAndMortyCharacterId();
setValue("rickAndMortyId", randomId);
setIdQuery(randomId);
}}
>
Random
</button>
</div>
<div>
{isLoading && <p>Loading...</p>}
<RickAndMortyInfo rickAndMortyCharacter={rickAndMortyCharacter} />
</div>
<ReactQueryDevtools initialIsOpen={true} />
</ErrorBoundary>
</div>
);
In this code:
const [rickAndMortyCharacter, setRickAndMortyCharacter] = useState({});
const handleRickAndMortyFetch = () => {
return delay()
.then(() => axio('...'))
.then((res) => setRickAndMortyCharacter(res.data));
};
const { isLoading, error } = useQuery(
["rickandmorty", idQuery],
handleRickAndMortyFetch
);
After a successful fetch, you assign the result to a local state instead of handing to react-query, so it doesn't know what data should be cached. You need to return the result like this:
const handleRickAndMortyFetch = () => {
return delay()
.then(() => axio('...'))
.then((res) => res.data); // return instead of calling setState()
};
So react-query can 'catch' it and put it into cache. The next step is to remove your local state rickAndMortyCharacter and reference data instead. It is the resolved result after a successful fetch.
const {
isLoading,
error,
data // remove rickAndMortyCharacter and reference this instead
} = useQuery(
["rickandmorty", idQuery],
handleRickAndMortyFetch
);
Only then can you log the cache as normal:
queryClient.getQueryData(["rickandmorty", 518]);
Live Demo
My ArticleList component is successfully getting & displaying the user's list of articles from firestore when I first load the app. The user can click a "Remove Article" button, which successfully removes the article from the subcollection in firestore, but it causes an error in the rendering of the react component, which seems to still be trying to render the article that was just removed and is now null. Is there something else I can do to make my react component continuously listen to the firestore data? If possible, I'd like to keep this a functional component and use hooks rather than making it a class, but I'm still learning how to use react hooks and therefore struggling a bit.
ArticleList component:
const ArticleList = (props) => {
const firestore = useFirestore();
const userId = props.auth.uid;
useFirestoreConnect([
{
collection: 'users',
doc: userId,
subcollections: [{collection: 'articles'}],
storeAs: userId + '::articles'
}
]);
const myArticles = useSelector(state => state.firestore.data[`${userId}::articles`]);
const dispatch = useDispatch();
const removeArticle = useCallback(
articleId => dispatch(removeArticleFromFirebase({ firestore }, articleId)),
[firestore]
);
if (props.auth.uid) {
return(
<div>
<h3>My Articles</h3>
<p>Currently signed in: {props.auth.email}</p>
<br/>
{myArticles ? (
Object.keys(myArticles).map(articleId => {
let article = myArticles[articleId];
let articleInformation = '';
if (articleId === props.currentPaperId) {
articleInformation =
<div>
<p>{article.year}</p>
<p>{article.description}</p>
<a target="_blank" href={article.downloadUrl}><button className='waves-effect waves-light btn-small'>See article</button></a>
<button className='waves-effect waves-light btn-small' onClick={() => {removeArticle(articleId);}}>Remove from My Articles</button>
</div>;
}
let authorName = '';
if (article.author) {
authorName = ` by ${article.author}`;
}
if (article) {
return <span key={articleId}>
<li onClick={() => {dispatch(selectArticle(articleId));}}>
<em>{article.title}</em>{authorName}
</li>{articleInformation}
</span>;
} else {
return null;
}
})
) : (
<h4>No articles yet</h4>
)
}
</div>
);
} else {
return null;
}
};
const mapStateToProps = (state) => {
return {
currentPaperId: state.currentPaperId,
auth: state.firebase.auth
};
};
export default compose(connect(mapStateToProps))(ArticleList);
And the removeArticleFromFirebase action:
export const removeArticleFromFirebase = ({ firestore }, id) => {
return (dispatch, getState) => {
const userId = getState().firebase.auth.uid;
firestore
.collection('users')
.doc(userId)
.collection('articles')
.doc(id)
.delete()
.then(() => {
console.log('Deleted article from firestore: ', id);
dispatch({ type: 'REMOVE_ARTICLE', id });
})
.catch(err => {
console.log('Error: ', err);
});
};
}
I've tried adding useState and useEffect in the ArticleList as follows (and tried having the component's return statement map through myArticlesState instead of myArticles), but no success:
const [myArticlesState, setMyArticlesState] = useState(myArticles);
useEffect(() => {
setMyArticlesState(myArticles);
}, [myArticles]);
Note: I do not currently have this article list in overall app state/redux store/props at all. This is something I was thinking of trying next, but I decided to post my question first in case I can just use hooks in this component. No other components/parts of the app need access to this particular list.
Console errors:
error image 1
error image 2
Github repo: https://github.com/jpremmel/yarp2.0
It's kind of difficult to see what's going on but it appears as though you are trying to use a property on an object that does not exist. Therefore, checking for those properties should help resolve this.
Can you try the follow code as your ArticleList?
const ArticleList = (props) => {
const firestore = useFirestore();
const userId = props.auth.uid;
useFirestoreConnect([{
collection: 'users',
doc: userId,
subcollections: [{ collection: 'articles' }],
storeAs: userId + '::articles'
}]);
const myArticles = useSelector(state => state.firestore.data[`${userId}::articles`]);
const dispatch = useDispatch();
const removeArticle = useCallback(articleId => dispatch(removeArticleFromFirebase({ firestore }, articleId)), [firestore]);
if (props.auth.uid) {
return (
<div>
<h3>My Articles</h3>
<p>Currently signed in: {props.auth.email}</p>
<br />
{myArticles ? (
Object.keys(myArticles).map(articleId => {
let article = myArticles[articleId];
let articleInformation = '';
if (article) {
if (
articleId === props.currentPaperId &&
article.hasOwnProperty('year') &&
article.hasOwnProperty('description') &&
article.hasOwnProperty('downloadUrl')
) {
articleInformation =
<div>
<p>{article.year}</p>
<p>{article.description}</p>
<a target="_blank" href={article.downloadUrl}><button className='waves-effect waves-light btn-small'>See article</button></a>
<button className='waves-effect waves-light btn-small' onClick={() => { removeArticle(articleId); }}>Remove from My Articles</button>
</div>;
}
let authorName = '';
if (article.hasOwnProperty('author') && article.author) {
authorName = ` by ${article.author}`;
}
if (article.hasOwnProperty('title') && article.title) {
return <span key={articleId}>
<li onClick={() => { dispatch(selectArticle(articleId)); }}>
<em>{article.title}</em>{authorName}
</li>{articleInformation}
</span>;
} else {
return null;
}
}
})
) : (
<h4>No articles yet</h4>
)
}
</div>
);
} else {
return null;
}
};
const mapStateToProps = (state) => {
return {
currentPaperId: state.currentPaperId,
auth: state.firebase.auth
};
};
export default compose(connect(mapStateToProps))(ArticleList);
Can you show us the error? I think it's about the state not being an array after you delete your data just initialize your state with an empty array like this :
Const= [articlesdata,setArticlesData]=useState([])
And leave the useEffect as it is