Updating React state based on what's in the database - javascript

Hi I'm new to Node/React, and I'm creating a learning project. It's platform that connects freelancers with nonprofit companies. I would like users to click a button to connect to a company. Once this is clicked, the user will have that company added as a connection in the database, and the button will become disabled and remain disabled in future user sessions.
Right now adding the user<->company connection to the database is working, and the "connect" button is working. Once clicked, it become disabled like I wanted to. I have React state in place. The problem is once the user logs out and comes back in, and views a company he's already connected to, the "connect" button is available to be clicked again.
How do I change the React state based on what's in the database? How do I make it so that if the user is already connected to this one company from an older session, the 'connect' button will remain disabled? This is what I have so far (shortened):
schema
CREATE TABLE companies (
company_handle VARCHAR(25) PRIMARY KEY,
password TEXT NOT NULL,
company_name TEXT NOT NULL,
num_employees INTEGER CHECK (num_employees > 0),
);
CREATE TABLE users (
username VARCHAR(25) PRIMARY KEY,
password TEXT NOT NULL,
email TEXT NOT NULL CHECK (position('#' IN email) > 1),
skill TEXT NOT NULL
);
CREATE TABLE connections (
username VARCHAR(25)
REFERENCES users ON DELETE CASCADE,
company_handle VARCHAR(25)
REFERENCES companies ON DELETE CASCADE,
PRIMARY KEY (username, company_handle)
);
App.js
function App() {
const [currentUser, setCurrentUser] = useState(null);
const [token, setToken] = useLocalStorage(TOKEN_LOCAL_STORAGE_ID);
const [connectionHandles, setConnectionHandles] = useState(new Set([]));
function hasConnectedToCompany(companyHandle) {
return connectionHandles.has(companyHandle);
}
function connectToCompany(companyHandle) {
if (hasConnectedToCompany(companyHandle)) return;
VolunteerApi.connectToCompany(currentUser.username, companyHandle);
setConnectionHandles(new Set([...connectionHandles, companyHandle]));
}
return (
<BrowserRouter>
<UserContext.Provider value={{ currentUser, setCurrentUser, hasConnectedToCompany, connectToCompany }}>
<div>
<Navigation logout={logout} />
<Routes loginUser={loginUser} signupUser={signupUser} loginCompany={loginCompany} signupCompany={signupCompany} />
</div>
</UserContext.Provider>
</BrowserRouter>
);
}
CompanyDetail.js
function CompanyDetail() {
const { companyHandle } = useParams();
const [company, setCompany] = useState(null);
const { currentUser, hasConnectedToCompany, connectToCompany } = useContext(UserContext);
const [connected, setConnected] = useState();
const [formErrors, setFormErrors] = useState([]);
React.useEffect(function updateConnectedStatus() {
setConnected(hasConnectedToCompany(companyHandle));
}, [companyHandle, hasConnectedToCompany]);
useEffect(function getCompanyDetail() {
async function getCompany() {
setCompany(await VolunteerApi.getCurrentCompany(companyHandle));
}
getCompany();
}, [companyHandle]);
async function handleConnect(evt) {
if (hasConnectedToCompany(companyHandle)) return;
connectToCompany(companyHandle);
setConnected(true);
let connectUserInDb;
try {
connectUserInDb = await VolunteerApi.connectToCompany(currentUser.username, companyHandle);
} catch (err) {
setFormErrors(err);
return;
}
}
if (currentUser) {
return (
<div>
<h1>{company.companyName}</h1>
<h4>{company.numEmployees}</h4>
<button onClick={handleConnect} disabled={connected}> {connected ? "Connected" : "Connect"} </button>
</div>
);
}
}

What you could do is make an API call after the user logs in to retrieve all the companies that a user is connected to from the database. Store the results in react state so your app will then be able to check whether or not the user has connected to this company before.

Related

Issue with 1st render of async local JSON data

I'm having an issue when it comes to rendering some data out in my React app. I have a page /users that renders a list of users and clicking on a specific user routes the client to a page with more info on that user /users/:id. That page has a component with user data inside of it and that data is being fetched from a local JSON file. The issue is that when I click on one of the users from the /users page, the page for that specific user breaks. Console is telling me userData is undefined.
I have attempted to render the specific user page once the data has been fetched but I don't think I'm doing it correctly. I have tried setting an isLoading state with useState as well as conditionally rendering the component based on the state of the data being fetched but I'm still not having much luck. Below is the User page and the UserInfo component.
User page
function User() {
const [userData, setUserData] = useState([]);
const { id } = useParams();
const fetchData = async () => {
const response = await fetch(`../data/userData/${id}.json`);
const data = await response.json();
setUserData(data);
};
useEffect(() => {
fetchData;
}, []);
return (
<div>
{userData ? (
<UserInfo userData={userData} />
) : (
<>
<h1>Loading...</h1>
</>
)}
</div>
);
}
UserInfo component
function UserInfo({ userData }) {
return (
<div className='userInfo__details'>
<div className='userInfo__name'>
<h1>{userData[0].name}</h1>
</div>
</div>
);
}
The page is being rendered before the userData is being retrieved by the fetch request and that's causing the page to break. Ideally I'd like to have a loading spinner or something while the data is retrieved but anytime I've been able to having else condition render to indicate the data is still being fetched, it just hangs there and never actually renders the page with the fetched data.
Any help is appreciated.
[] as your default state is will return as true which renders the UserInfo component before time.
You can do this instead
return (
<div>
{!!userData.length ? (
<UserInfo userData={userData} />
) : (
<>
<h1>Loading...</h1>
</>
)}
</div>
);

Issues rendering the right dashboard UI user based on roles

I am trying to render UI in my project based on selected roles (brands, agency, influencer) on click. However, the logic that I am putting together is not loading the right UI and I don't quiet understand why not.
I have tried moving the role and setRole to the top component and passed the props down to the child components that read role and updated it via setRole so that I can have the state to be available in two places.
I also set a logic that should display components based on if the role equals the value of the buttons.
What happened was the components weren't loading upon clicking the function that handles click. However, logging out to the console if the role equals the value of the clicked button returns true, the right string that the logic was based on.
What I am expecting to happen is to load the component e.g: "Brands" when users click and select "brands" which is the value of the clicked button. Vise versa for the other components.
My code is as follows:
import { useState } from 'react';
import { useSession } from 'next-auth/react';
import Brands from './Brands';
import Agency from './Agency';
import CreatorsDash from './CreatorsDashboard';
export default function FirstPageModal({ role: userRole }) {
const [role, setRole] = useState(userRole);
const { data: session } = useSession();
const handleClick = (e) => {
e.preventDefault();
let buttonValue = e.target.value;
const clickedRole = role?.map((user) => {
let { role } = user;
if (buttonValue) {
userRole = { role: buttonValue };
}
return { userRole };
});
console.log(clickedRole); //Returns an array
clickedRole.map((item) => {
const { role } = item.userRole;
console.log(role); //Returns string ("agency" / "brands" / "Influencer")
if (session && role === 'brands') {
console.log(role); //This logs "brands" as expected but doesn't return the component
// return <Brands session={session} role={role} />;
} else if (session && role === 'agency') {
return <Agency session={session} role={role} />;
} else if (session && role === 'Influencer') {
return <CreatorsDash session={session} role={role} />;
} else {
console.log('Select a role');
}
});
};
return (
<>
<div className="">
<button type="button" className="" onClick={handleClick} value="agency">
As an Agency
</button>
<button type="button" className="" onClick={handleClick} value="brands">
As a Brand
</button>
<button
type="button"
className=""
onClick={handleClick}
value="Influencer"
>
As an Influencer
</button>
</div>
</>
);
}
Returning a component from an onClick handler doesn't automatically render the component. One thing you could do is to keep track of the role in the state and then put the <Brands /> <Agency/> and <CreatorsDash /> components in the render function and dynamically show/hide them like {role === "brands" && <Brands />. This can also be done with css, although the benefits of this are not so clear,.
Side note, it is very helpful to post a codepen with your code, especially as your code gets more complicated

Static generation and SWR in single page

I'm building a blog website in Next.js, the API for the blog is from some headless CMS.
In a page I want to do the following:
List some blogs.
Set of buttons available, based on each button click different set of blogs are loading (should replace the blogs in #1).
Since SEO is needed I'm pretty confused to use which approach should I choose.
What I thinking that I generate the initial list with
getStaticProps (Static Generation), and after loading I want to replace the blogs based on user action (button click).
But I'm confused, is it possible to use static generation and SWR in single page?
Here is my implementation.
pages/index.js
export async function getStaticProps() {
const resPosts = await fetch(`${process.env.API_BASE_URL}posts?per_page=4&&_embed`)
const posts = await resPosts.json()
return {
props: {
posts
},
revalidate:10
}
}
export default function Home({posts}) {
return (
<div>
//pass data to FeaturedBlogs component (Components/featuredBlogs.js)
<FeaturedBlogs categories={categories} posts={posts} />
</div>
)
}
Components/featuredBlogs.js
const FeaturedBlogs = ({posts }) => {
return (
<div className={`${blogStyles.feature_blogs_wrap}`}>
//need to load the below blogs based on button click
<button onClick={handleClick('health')}>Health</button>
<button onClick={handleClick('latest')}>Latest</button>
//listing blogs
{posts.map((item ) => (
<Link key={item.id} href="/blog/view" passHref={true}>
<section>
<Image alt="blog_img" src={item._embedded['wp:featuredmedia'][0].media_details.sizes.medium.source_url} width="200" height="200" />
<div className={`${blogStyles.feature_blogs_content}`}>
<div className={`${blogStyles.feature_blogs_label}`}>
<span>{item._embedded['wp:term'][0][0].name}</span>
</div>
<p>{item.title.rendered}</p>
<div className={`${blogStyles.feature_blogs_author}`}>
<Image alt="author" src={item._embedded.author[0].avatar_urls[48]} width="200" height="200" />
<span>{item._embedded.author[0].name}</span>
</div>
</div>
</section>
</Link>
))}
</div>
)
}
const handleClick = (id) => {
//console.log(id)
}
What I need is to load the blogs in handleClick event, but the problem is this will not work since it's generated from the server at build time.
In the FeaturedBlogs component, you can create a state variable to keep track when a new category is selected on the client-side.
const [category, setCategory] = useState()
You can then make useSWR conditionally fetch data based on the value of this category variable.
const { data, loading } = useSWR(category ? [category] : null, fetcher)
The fetcher function would have the logic to fetch the posts for a given category.
const fetcher = async (category) => {
const response = await fetch(/* Endpoint to get posts for given category */)
return await response.json()
}
With this in place, you can have the component render the posts retrieved in getStaticProps as a default, when category is not set. This would happen on the initial render of the page. However, when a button is clicked, and category gets set, that category's data will be fetched and rendered instead.
Here's the full code of a modified version of your original component.
// Components/featuredBlogs.js
const fetcher = async (category) => {
const response = await fetch(/* Endpoint to get posts for given category */)
return await response.json()
}
const FeaturedBlogs = ({ posts }) => {
// Add state variable to keep track of the selected category
const [category, setCategory] = useState()
// Fetch posts from category only if `category` is set
const { data, loading } = useSWR(category ? [category] : null, fetcher)
const handleClick = (cat) => () => {
setCategory(cat)
}
// If `category` is set render data with post for given category, otherwise render all posts from `getStaticProps`
const itemsToRender = category ? data : posts
return (
<div className={blogStyles.feature_blogs_wrap}>
<button onClick={handleClick('health')}>Health</button>
<button onClick={handleClick('latest')}>Latest</button>
{loading && <div>Loading...</div>}
{!!itemsToRender?.length && itemsToRender.map((item) => (
<!-- Render items here -->
))}
</div>
)
}

React state lost when user refreshes page

I'm new to React/Node and working on a learning project. It's a platform that connects users (freelancers) with nonprofit companies. When a user logs in, they can view a list of companies and click a button to connect with that company. They can then go to the UserConnections page to view all the companies they connected with.
When they click the 'connect' button, the connection is made in the database, and the button becomes disabled. This is currently working correctly.. unless you refresh the page, in which case the button becomes clickable again.
I'm probably not using state correctly. I'm tracking two different states. The first is when it's a "fresh" connection the user just made. The second is when they visit their UserConnections page, I'm retrieving their "old" connections from the database.
What can I do to make sure the state of a connection persists if the user refreshes the page, or if they come back later? My code is below (shortened to only include relevant code)
App.js
function App() {
const [currentUser, setCurrentUser] = useState(null);
const [connectionHandles, setConnectionHandles] = useState([]);
// Check if connected to this company
function hasConnectedToCompany(companyHandle) {
return connectionHandles.includes(companyHandle);
}
// Make the connection in the database
function connectToCompany(companyHandle) {
if (hasConnectedToCompany(companyHandle)) return;
VolunteerApi.connectToCompany(currentUser.username, companyHandle);
setConnectionHandles([...connectionHandles, companyHandle]);
}
return (
<BrowserRouter>
<UserContext.Provider value={{ connectionHandles, setConnectionHandles, currentUser, setCurrentUser, hasConnectedToCompany, connectToCompany }}>
<div>
<Navigation />
<Routes />
</div>
</UserContext.Provider>
</BrowserRouter>
);
}
CompanyDetail.js
function CompanyDetail() {
const { companyHandle } = useParams();
const [company, setCompany] = useState(null);
const { currentUser, hasConnectedToCompany, connectToCompany } = useContext(UserContext);
const [connected, setConnected] = useState();
React.useEffect(function updateConnectedStatus() {
setConnected(hasConnectedToCompany(companyHandle));
}, [companyHandle, hasConnectedToCompany]);
// Handle connect
async function handleConnect(evt) {
if (hasConnectedToCompany(companyHandle)) return;
connectToCompany(companyHandle);
setConnected(true);
let connectUserInDb;
try {
connectUserInDb = await VolunteerApi.connectToCompany(currentUser.username, companyHandle);
} catch (err) {
setFormErrors(err);
return;
}
}
if (currentUser) {
return (
<div>
<h1>{company.companyName}</h1>
<p>
<button onClick={handleConnect} disabled={connected}> {connected ? "Connected" : "Connect"} </button>
</p>
</div>
);
}
}
UserConnections.js
function UserConnections() {
const { currentUser, connectionHandles } = useContext(UserContext);
const [companies, setCompanies] = useState([]);
useEffect(() => {
let isMounted = true;
const connections = currentUser.connections.concat(connectionHandles);
const comps = connections.map((c) => VolunteerApi.getCurrentCompany(c));
Promise.all(comps).then(comps => isMounted && setCompanies(comps));
return () => { isMounted = false };
}, [currentUser, connectionHandles]);
if (!companies || companies.length === 0) {
return (
<div>
<div>
<p>You have no connections</p>
</div>
</div>
)
} else {
return (
<div>
<div>
<h1>Connections</h1>
{companies && companies.map(c => (
<CompanyCard
key={c.companyHandle}
companyHandle={c.companyHandle}
companyName={c.companyName}
/>
))}
</div>
</div>
);
}
};
the state will reset when the page refreshes, but you can either use the DB to save the connection, or use local storage.
Indeed, State is lost with React when you Refresh the page.
You should use another method to keep this information even if the user refresh the page. Here are the options:
In the database. Could work if the user is behind an authentification (Login password). Example User name: Paul, id; 3131, did click on the button "connect". In your database, you add a column in the table User called userConnect = true
In the url. As soon as the user click on the button "connect" you change the URL with React router. For example the URL was mydomain.com, and after clicking it becomes mydomain.com?clicked=true. If the user refresh your page, you still have the information about the user who clicked on the button.
In a cookie (More info here https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies)
In local Storage (More info here https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage/local)

"Cannot read property 'length' of undefined" on an empty array

Hi I'm new to Node/React, and I'm creating a learning project. It's platform that connects freelancers with nonprofit companies. Users (freelancers) view a list of companies, and click a button to connect to a company. Once this is clicked, the user will have that company added as a relationship in the database. This is working correctly.
Now I'm trying to have a page where the user can view all their connections (the companies they connected with). The solution below works but only if the user has at least one connection. Otherwise, I get the error Cannot read property 'length' of undefined.
To figure out which JSX to render, I'm using a conditional to see if the user has connections. If not, I wanna show "You have no connections". I'm doing this by checking if (!companies.length) then show "you have no connections". companies is set as in empty array in the state. I don't understand why it's undefined. Even if the user has no connections, companies is still an empty array. so why why would companies.length return this error? How can I improve this code to avoid this problem?
function UserConnections() {
const { currentUser } = useContext(UserContext);
const connections = currentUser.connections;
const [companies, setCompanies] = useState([]);
useEffect(() => {
const comps = connections.map((c) => VolunteerApi.getCurrentCompany(c));
Promise.all(comps).then((comps => setCompanies(comps)));
}, [connections]);
if (!companies.length) {
return (
<div>
<p>You have no connections</p>
</div>
)
} else {
return (
<div>
{companies.map(c => (
<CompanyCard
key={c.companyHandle}
companyHandle={c.companyHandle}
companyName={c.companyName}
country={c.country}
numEmployees={c.numEmployees}
shortDescription={c.shortDescription}
/>
))}
</div>
);
}
};
Edit: Sorry, I should've included that the error is being thrown from a different component (UserLoginForm). This error is thrown when the user who has no connections logs in. But in the UserConnections component (code above), if I change if (!companies.length) to if (!companies), the user can login fine, but UserConnections will not render anything at all. That's why I was sure the error is refering to the companies.length in the UserConnections component.
The UserLoginForm component has been working fine whether the user has connections or not, so I don't think the error is coming from here.
UserLoginForm
function UserLoginForm({ loginUser }) {
const [formData, setFormData] = useState({
username: "",
password: "",
});
const [formErrors, setFormErrors] = useState([]);
const history = useHistory();
// Handle form submission
async function handleSubmit(evt) {
evt.preventDefault();
let results = await loginUser(formData);
if (results.success) {
history.push("/companies");
} else {
setFormErrors(results.errors);
}
}
// Handle change function
function handleChange(evt) {
const { name, value } = evt.target;
setFormData(d => ({ ...d, [name]: value }));
}
return (
<div>
<div>
<h1>User Login Form</h1>
<div>
<form onSubmit={handleSubmit}>
<div>
<input
name="username"
className="form-control"
placeholder="Username"
value={formData.username}
onChange={handleChange}
required
/>
</div>
<div>
<input
name="password"
className="form-control"
placeholder="Password"
type="password"
value={formData.password}
onChange={handleChange}
required
/>
</div>
{formErrors.length
? <Alert type="danger" message={formErrors} />
: null
}
<button className="btn btn-lg btn-primary my-3">
Submit
</button>
</form>
</div>
</div>
</div>
);
};
Edit 2: The solutions provided in this thread actually solved the problem. The error message kept persisting due to a different problem coming from another component.
Change this:
{companies.map(c => (...)}
to this:
{companies && companies.map(c => (...)}
and this:
if (!companies.length) {
to this:
if (!companies || companies.length === 0) {
This will then check for a nullish value, before running the map operation or checking length.
Even if the user has no connections, companies is still an empty array. so why why would companies.length return this error?
Your assumption that companies is an empty array is incorrect. Somehow it is set to undefined. You can either protect against this by doing
if (!companies || companies.length == 0) {
or by making sure companies is always set to an array.

Categories

Resources