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

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.

Related

Using jest to send text to an input field and update state

Working on a technical challenge again. I want to set up unit tests (don't have to, but I would like to).
Problem is when I try to use userEvent or fireEvent to send some text to an input field, nothing happens (my expect toEqual test fails. I noticed that when I commented out enough of my code to figure out what was going on, I got the error: TypeError: setUsername is not a function when I use userEvent to type some text into the input field.
I assumed this meant I would need to create a mock state (setUsername updates the username state). However, this didn't resolve the issue.
The component I am testing is a child of a parent component Index . So I also tried wrapping this child component under the parent component (the username state is passed down to the child from Index). This didn't resolve the issue.
Here's my code:
const one = <index>
<One/>
</index>
describe("Step One", () => {
it("Can input text", async () => {
render(one);
const mockSetState = jest.fn();
const component ={
setState: mockSetState,
state: {
username: "test"
}
};
const username = screen.getByTestId("username");
const email = screen.getByTestId("email");
await userEvent.type(username, "Emmanuel");
userEvent.type(email, "emmanuelsibanda21#gmail.com")
expect(mockSetState).toEqual({"username": "emmanuelsibanda21#gmail.com"});
// expect(email.value).toEqual("emmanuelsibanda21#gmail.com")
});
});
This is the code in Index:
const [username, setUsername] = useState('')
const [email, setEmail] = useState(null);
return (
...
{
steps === 1 ?
<One
username={username}
setUsername={setUsername}
steps={steps}
setSteps={setSteps}
setSelectedCountry={setSelectedCountry}
selectedCountry={selectedCountry}
setSelectedState={setSelectedState}
selectedState={selectedState}
setSelectedCity={setSelectedCity}
selectedCity={selectedCity}
email={email}
setEmail={setEmail}
profilePic={profilePic}
setProfilePic={setProfilePic}
/> ...
}
Here's the code from One:
export default function One ({username, setUsername, steps, setSteps, setEmail, email, profilePic, setProfilePic}) {
const url = 'http://localhost:3000/api/getPic';
function changeHandler(e) {
setUsername(e.target.value)
}
function emailChange(e){
setEmail(e.target.value)
}
...
return (
<>
...
<div className={styles.description}>
<Input
data-testid="username"
onChange={changeHandler}
placeholder="What is your full name"
value={username}
/>
<Input
data-testid="email"
onChange={emailChange}
placeholder="Please enter a valid email"
value={email}
/>
</div>
{
username && isEmailValid(email) === true?
<Button data-testid="next" onClick={ () => nextStep() }>Next</Button>
: null
}
</main>
</>
)
};
Any ideas

map() shows new messages from state only if I type something into input

Every time I send a message, map() only shows my new message when I type something in the input (re-render).
The listener works fine, it displays new messages in the console immediately after sending a new message, curiously, the messages state also updates when I look in React Developers Tools,
But useEffect[messages] does not trigger on a new message, and it only displays new messages after I type something in the input.
Here is my component with comments:
import { useState, useEffect, useRef } from "react";
const ChatWindowChannel = ({ window, chat }) => {
const [messages, setMessages] = useState([]);
const [message, setMessage] = useState("");
const loaded = useRef(null);
const messagesEndRef = useRef(null);
const channelMessagesHandler = async () => {
const channelMessagesListener = await chat.loadMessagesOfChannel(window);
channelMessagesListener.on((msgs) => {
console.log(msgs); // Here shows new messages after I click `send`
// Works correct.
setMessages(msgs); // In React Developers Tools the state `messages` are update if I click `send`
// Works correct.
console.log(messages); // Shows always empty array[]
// Dont works correct
});
};
async function send() {
await chat.sendMessageToChannel(window, message, {
action: "join",
alias: chat.gun.user().alias,
pubKey: chat.gun.user().is.pub,
name: "grizzly.crypto",
});
setMessage("");
}
useEffect(() => {
// Shows only once. It shold every time on new message
console.log("Messages changed");
}, [messages]);
useEffect(() => {
loaded.current !== window.key && channelMessagesHandler();
loaded.current = window.key;
}, [window]);
return (
<div>
<div className="ChatWindow">
<h2>
Channel {window.name} - {window.isPrivate ? "Private" : "Public"}
</h2>
<div>
<details style={{ float: "right" }}>
<summary>Actions</summary>
<button
onClick={async () => {
await chat.leaveChannel(window);
}}
>
Leave channel
</button>
</details>
</div>
<div className="msgs">
{messages.map((message, key) => (
<div key={`${key}-messages`}>
<small>{message.time}</small>{" "}
<small>{message.peerInfo.alias}</small> <p>{message.msg}</p>
<div ref={messagesEndRef} />
</div>
))}
</div>
<div>
<div>
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") send();
}}
/>
</div>
<div>
<button onClick={send}>Send</button>
</div>
</div>
</div>
</div>
);
};
export default ChatWindowChannel;
Note 1: setMessages(msgs); do not change the value of messages immediately, it will be set only on next render so console.log(messages) just after setting will give you incorrect results.
Note 2: In case the reference of the array msgs that you are trying to set to a state variable is not changed even if array is modified - re-render will not be executed and useEffects will not be triggered, reference to an array needs to be new.
setMessages([...msgs]);
Same happens also with {objects}.
Can you try something like this:
setMessages([...msgs]);
instead of setMessages(msgs);
State updates in React are asynchronous; when an update is requested, there is no guarantee that the updates will be made immediately. The updater functions enqueue changes to the component state, but React may delay the changes, updating several components in a single pass. In that case you can't view (console.log) immediately after using state setter function.

see element information by id with strapi and react

I'm learning react with strapi.
I have elements in a database and I manage to display them all without any problem.
I would like that when we click on an element we see the information of this one.
So I retrieve the link id without problem and I manage to display the information except that it only works once. In fact, if I click on the second element I have an error message.
Here is the code
const Articles = ({animal}) => {
const [error, setError] = useState(null);
const [Animaux, setAnimaux] = useState([]);
const { id } = useParams()
useEffect(() => {
axios
.get(`http://localhost:1337/api/animaux/`+id)
.then(({ data }) => setAnimaux(data))
.catch((error) => setError(error))
}, [id])
if (error) {
// Print errors if any
return <div>An error occured: {error.message}</div>;
}
console.log(Animaux)
return(
<div>
Id : {Animaux.data.id}
Nom : {Animaux.data.attributes.nom}
Description : {Animaux.data.attributes.Description}
<div key={animal.id} className="card" >
<span className="btn btn-primary">Voir {animal.attributes.nom}</span>
</div>
)}
*/}
</div>
)
};
export default Articles;
Here is the result the first time :
Here is the result the first time
The result that I have afterwards (after updating, then it does not come back at all)
The result after
I see that it returns me an empty array but I don't understand why
Many thanks to all for your future help.
Data which you get from api returns empty array. In addition, you cannot access the object of the array without giving an index.
You can't use like that {Animaux.data.id}, you can return with map ;
{
Animaux.map(function(data,index){
return(
<>
<p> id : {data.id} </p>
<p> nom : {data.nom} </p>
<p> Description : {data.Description} </p>
</>
)
})
}
Can you trying using the optional chaining operator.
return(
<div>
Id : {Animaux?.data?.id}
Nom : {Animaux?.data?.attributes?.nom}
Description : {Animaux?.data?.attributes?.Description}
<div key={animal?.id} className="card" >
<span className="btn btn-primary">Voir {animal.attributes.nom}</span>
</div>
</div>

multi step form in react taking wrong validation

I am writing my problem as a fresh part here.
I made a multi step form where I have on dynamic filed in 1st form, that field is to create password manually or just auto generated.
So my multi step form is working fine going to and forth is fine, but I have to pass the fields to main component so it can check for validation, and I am passing that password too
Here comes the issue
When i pass the password field also then it takes the validation even when I have click on auto generated password
I am passing fields like this fields: ["uname", "email", "password"], //to support multiple fields form
so even not check on let me create password it takes the validation.
When i click let me create password and input some values then click on next and when I comes back the input field sets to hidden again to its initial state I know why it is happening, because when I come back it takes the initial state allover again.
i am fed-up with this thing for now, I have tried many things but didn't work below is my code
import React, { useState, useEffect } from "react";
import Form1 from "./components/Form1";
import Form2 from "./components/Form2";
import Form3 from "./components/Form3";
import { useForm } from "react-hook-form";
function MainComponent() {
const { register, triggerValidation, errors, getValues } = useForm();
const [defaultValues, setDefaultValues] = useState({});
const forms = [
{
fields: ["uname", "email", "password"], //to support multiple fields form
component: (register, errors, defaultValues) => (
<Form1
register={register}
errors={errors}
defaultValues={defaultValues}
/>
)
},
{
fields: ["lname"],
component: (register, errors, defaultValues) => (
<Form2
register={register}
errors={errors}
defaultValues={defaultValues}
/>
)
},
{
fields: [""],
component: (register, errors, defaultValues) => (
<Form3
register={register}
errors={errors}
defaultValues={defaultValues}
/>
)
}
];
const [currentForm, setCurrentForm] = useState(0);
const moveToPrevious = () => {
setDefaultValues(prev => ({ ...prev, ...getValues() }));
triggerValidation(forms[currentForm].fields).then(valid => {
if (valid) setCurrentForm(currentForm - 1);
});
};
const moveToNext = () => {
setDefaultValues(prev => ({ ...prev, ...getValues() }));
triggerValidation(forms[currentForm].fields).then(valid => {
if (valid) setCurrentForm(currentForm + 1);
});
};
const prevButton = currentForm !== 0;
const nextButton = currentForm !== forms.length - 1;
const handleSubmit = e => {
console.log("whole form data - ", JSON.stringify(defaultValues));
};
return (
<div>
<div class="progress">
<div>{currentForm}</div>
</div>
{forms[currentForm].component(
register,
errors,
defaultValues[currentForm]
)}
{prevButton && (
<button
className="btn btn-primary"
type="button"
onClick={moveToPrevious}
>
back
</button>
)}
{nextButton && (
<button className="btn btn-primary" type="button" onClick={moveToNext}>
next
</button>
)}
{currentForm === 2 && (
<button
onClick={handleSubmit}
className="btn btn-primary"
type="submit"
>
Submit
</button>
)}
</div>
);
}
export default MainComponent;
please check my code sand box here you can find full working code Code sandbox
React Hook Form embrace native form validation, which means when your component is removed from the DOM and input state will be removed. We designed this to be aligned with the standard, however we start to realize more and more users used to controlled form get confused with this concept, so we are introducing a new config to retain the unmounted input state. This is still in RC and not released.
useForm({ shouldUnregister: true })
Solution for now:
break into multiple routes and store data in the global store
https://www.youtube.com/watch?v=CeAkxVwsyMU
bring your steps into multiple forms and store data in a local state
https://codesandbox.io/s/tabs-760h9
use keepAlive and keep them alive:
https://github.com/CJY0208/react-activation

How to run onSubmit={} only after the props have updated?

I want to run onSubmit on my form only after my props that I receive from the reducer update.
const mapStateToProps = state => {
return {
authError: state.auth.authError // I want this to update before the action.
};
};
If I console.log authError in onSutmit handler I receive the old authError. The second time I run it, I receive the updated authError.
handleSubmit = e => {
e.preventDefault();
console.log("error exists?", this.props.authError);
this.props.signIn(this.state); //dispatches a login action
this.handleHideLogin(); // hides the form after action is dispatched
};
I want to hide the form only after the action is dispatched and the error is null. (it returns null automatically if the authentication succeeds)
I tried using setTimeout() and it technically works, but I want to know if there is a more "proper" way to do it.
handleSubmit = e => {
e.preventDefault();
this.props.signIn(this.state);
setTimeout(() => {
if (this.props.authError) {
console.log("returns error =>", this.props.authError);
} else {
console.log("no error =>", this.props.authError);
this.handleHideLogin();
}
}, 500);
};
part of my component for reference
<form onSubmit={!this.props.authError ? this.handleSubmit : null}>
//the above onSubmit is different if I use the setTimeout method.
<div className="modal-login-body">
<div className="modal-login-input">
<input
type="email/text"
name="email"
autoComplete="off"
onChange={this.handleChange}
required
/>
<label htmlFor="email" className="label-name">
<span className="content-name">EMAIL</span>
</label>
</div>
<div className="modal-login-input">
<input
type="password"
name="password"
autoComplete="off"
onChange={this.handleChange}
required
/>
<label htmlFor="password" className="label-name">
<span className="content-name">PASSWORD</span>
</label>
</div>
</div>
<div className="my-modal-footer">
<p className="login-failed-msg">
{this.props.authError ? this.props.authError : null}
</p>
<button type="submit" className="complete-login-button">
LOGIN
</button>
<a href="CHANGE" className="forgot-password">
<u>Forgot your password?</u>
</a>
</div>
</form>
I am assuming that this.props.signIn is an async function.
And thus this.props.authError is updated asynchronously and that's why if you setup the timeout it works in some cases (when you get the response shorter than 5 seconds).
One way to solve it is using then and catch without updating the state of the form
handleSubmit = e => {
e.preventDefault();
this.props.signIn(this.state).then(resp => {
this.setState({userIsValid: true, failure: null})
this.onUpdate(userIsValid);
})
.catch(err => {
this.setState({userIsValid: false, failure: "Failed to login"})
});
}
and use if-else to determine whether to show the form or to display your website
class App extends React.Component {
render() {
if (this.state.isValidUser) {
return <Main />
} else {
return <LoginForm onUpdate={(isValid) => {this.setState({isValidUser: isValid})} />
}
}
}
In other words, the LoginForm component stores username, password, failure (error message why login failed) and isValidUser (to determine if login is successful).
The App has a state to determine what to show, the website or the login component. Using onUpdate that is passed as props to the login component we can update the state of App and show the website if login is successful.
I hope it helps.
I solved this issue by conditonal rendering on the entire component.
I used Redirect from react-router-dom to just not render the element if I'm logged in.
If I'm logged in I don't need to display the login form anyway.
if (this.props.loggedOut) {
<form onSubmit={this.handleSubmit}>
//...
</form>
} else {
return <Redirect to="/" />;
}

Categories

Resources