How to render element conditionally in react - javascript

I am using mobx + react setup for this subpage to make a searchable list of users. My list of items is not rendering with if statement. In the solution I am trying to render one of two lists in my subpage. Depending on boolean 'isSearching'. First element should be shown when input field is empty, and second one is when input field has value written. They are same arrays, Only difference between lists arrays are that one of them is filtered.
Code:
<ul className='items__block'>
{
this.props.PeopleStore.people.isSearching = false ?
(this.props.PeopleStore.people.map(this.person))
:
(this.props.PeopleStore.searchingList.map(this.person))
}
</ul>
Althought if I remove condition, it works separated:
<ul className='items__block'>
{
this.props.PeopleStore.people.map(this.person)
}
</ul>
<ul className='items__block'>
{
this.props.PeopleStore.people.map(this.person)
}
</ul>
Store file:
import { runInAction, observable, action, toJS } from 'mobx';
// ES7 compiler
import regeneratorRuntime from 'regenerator-runtime';
class PeopleStore {
#observable people = [];
#observable loading = false;
#observable isSearching = false;
#observable searchingList = [];
// API call
loadPeople = async() => {
this.loading = true;
const response = await fetch('https://randomuser.me/api/?results=71');
const json = await response.json();
runInAction(() => {
this.people = json.results;
});
this.loading = false;
console.log(toJS(this.people));
}
// this function is called by onChange event
#action.bound filterList = textTyped => {
// changing boolean to identify if input is empty or not
if (textTyped.target.value.length < 1) {
this.isSearching = false;
} else {
this.isSearching = true;
}
console.log(this.isSearching);
let peoplesNames = [];
for (let i = 0; i < this.people.length; i++) {
peoplesNames.push(toJS(this.people[i]));
}
peoplesNames = peoplesNames.filter(function(item) {
return item.name.first.toLowerCase().search(textTyped.target.value.toLowerCase()) !== -1
});
this.searchingList = peoplesNames;
// tracking both arrays, they both work
console.log(toJS(this.searchingList));
console.log(toJS(this.people));
}
}
export default new PeopleStore();
Component file:
#inject('showHandler', 'PeopleStore') #observer
class PickList extends React.Component {
componentWillMount() {
this.props.PeopleStore.loadPeople();
}
person = ({name, picture}, index) =>
<li className="items__block--user" key={index} onClick={this.props.PeopleStore.selectPerson}>
<img className="user--image" src={picture.medium} alt="face" />
<span className="user--name">{`${name.first} ${name.last}`}</span>
</li>;
render() {
if (this.props.PeopleStore.loading) {
return (
<div className="loader"></div>
);
}
return (
<React.Fragment>
<input className="users__block--input" onChange={this.props.PeopleStore.filterList}></input>
<ul className='items__block'>
{
this.props.PeopleStore.people.isSearching = false //checks mobx prop
?
(this.props.PeopleStore.people.map(this.person))
:
(this.props.PeopleStore.searchingList.map(this.person))
}
</ul>
Why is it not working? On page render isSearching prop is set to false and that should effect the if statement as it is.

Issue is here, you are not checking the condition properly:
this.props.PeopleStore.people.isSearching = false
It should be:
this.props.PeopleStore.people.isSearching == false // notice "=="
See what will happen with =, it will assign the value returned by ternary operator expression to isSearching variable. It will be treated like this:
isSearching = (false? 1: 2); // isSearching will get a new value, and second expression will get executed always
Check this snippet:
let b = false;
b = false? 1: 2; //b will become 2
console.log('b = ', b);

Related

How to handle an object instance in the state?

I have a class like this:
class Outcome {
constructor(name) {
this.name = name;
this.inProgress = "";
this.success = null;
this.messages = [];
}
addMessage(type, text) {
this.messages.push({
type,
text
});
}
getMessagesByType(type) {
return this.messages.filter((message) => message.type === type);
}
}
In my React component I imported it, and I would like to use it like this:
submit() {
let outcome = new Outcome("submit");
outcome.inProgress = true;
this.setState({
outcome // Save in state so I can show a spinner
});
if (!formsDataValid) {
outcome.inProgress = false;
outcome.success = false;
outcome.addMessage("error", "Data are not valid");
this.setState({
outcome
});
return;
}
fetch().then((response) => {
outcome.inProgress = false;
if (response.ok) {
outcome.success = true;
outcome.addMessage("success", "Operation correctly performed");
} else {
outcome.success = false;
outcome.addMessage("error", response.error);
}
this.setState({
outcome
});
});
}
then in render I can check the result in this way:
render() {
{this.state.outcome?.inProgress ?
"Spinner here"
: this.state.outcome?.messages.length > 0 ?
"Here render the messages"
: null}
<button type="submit" disabled={this.state.outcome?.success || false}>Submit button</button>
}
This should works, but the problem is that in handle submit, when I'm doing for example outcome.success = false; it will edit the state directly, because the object is a reference.
Is there a clean way to do that without edit the state directly? I tried
this.setState({
outcome: { ...outcome }
});
but in this way it will remove the methods of the class in the object it clone in to the state.
I know we should use React Hooks, but the components is an old component and we have no time to change that.
One option would be to create a method that can produce a copy of an Outcome. Essentially when you do {...outcome} you're losing the prototype chain from outcome and only copying its members.
class Outcome {
constructor(name) {
this.name = name;
this.inProgress = "";
this.success = null;
this.messages = [];
}
addMessage(type, text) {
this.messages.push({
type,
text
});
}
getMessagesByType(type) {
return this.messages.filter((message) => message.type === type);
}
clone() {
const result = new Outcome(this.name);
result.inProgress = this.inProgress;
result.success = this.success;
result.messages = this.messages;
return result;
}
}
You could then use it like this:
this.setState({
outcome: outcome.clone();
});
There's also a generic way to do this that'll work for (most) classful objects.
function cloneInstance(obj) {
return Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
}
If you want to be able to easily mutate the result too for more of a functional style, I'd probably use something like this:
function immutableModify(obj, cb = o => o) {
const result = Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
);
cb(result);
return result;
}
which can then be used like
this.setState({
outcome: immutableModify(outcome, outcome => outcome.name = "test")
});

Call a function inside const React/Typescript

In my application, I want to conditionally render something, so I made a function getItem which I want to call in my custom Tooltip, const CustomTooltip.
How can I call the function with the return of my custom tooltip? Currently, it render getState("A"),
getState("B"),
getState("C"), on the tooltip. See code below:
const numberStates = 3;
function getState({payload}: TooltipProps<ValueType, NameType>, state: string ){
if(payload){
for(let i = 0; i < numberStates; i++){
if(payload[i].dataKey == state){
return <p>{ payload[i] } : { payload[i].value }</p>
}
}
}
return null;
}
const CustomTooltip = ({ active, payload, label}: TooltipProps<ValueType, NameType>) => {
if(active && payload && payload.length){
return (
<div className = "custom-tooltip">
getState("A")
getState("B")
getState("C")
</div>
);
}
return null;
}
You need to surround your calls to getState in brackets:
const CustomTooltip = ({ active, payload, label}: TooltipProps<ValueType, NameType>) => {
if(active && payload && payload.length){
return (
<div className = "custom-tooltip">
{getState("A")}
{getState("B")}
{getState("C")}
</div>
);
}
return null;
You should expect to use this syntax if you're trying to utilize any javascript functionalities within the JSX.
More information can be found here regarding functions in react. You can scroll down to the "Example: Passing params using arrow functions" section and see an example of how brackets are necessary for using a map on the state.

I'm getting an object even though I'm returning an array from my javascript function

I have a function in a javascript file where I return an array. But when I call this function, when I look at the type with the "typeof" command, it returns an object instead of an array.
My javascript file is here.
import {useStore} from "vuex";
import {computed} from "vue";
export const getActions = (menuId) => {
const store = useStore()
const loginInfo = computed(() => {
return store.state.Identity.loginInfo
});
const actions = []
loginInfo.value.Authorization.forEach((x)=>{
let splitData = x.Id.split('-')
if(splitData[0] === '02' && splitData[1] === menuId){
if(!actions.some(item => item.Id === splitData[2]))
actions.push({
Id:splitData[2],
Definition: x.Definition,
Clicked:false
})
}
})
return actions;
}
Here is where I call and use this function.
let actions =[]
actions = getActions(props.menuId)
for(let i=0; actions.length;i++){
if(props.actionId === actions[i].Id)
return isAuth.value = false
else
isAuth.value = true
}
Although my variable named actions is an array, it sees it as an object and my computer starts freezing. My computer's fan starts running very fast and chrome starts to freeze.
You didn't set your loop right:
for(let i = 0; i < actions.length; i++){

useState updates correctly when adding to array, but not removing

I have a simple component that allows me to select an item from a list, then remove an item from a list. I display the active list within a parent component. No matter what I do or how I approach it, the removal of an active component is never updated unless they are all in active.
Here is a smaller (yet large snippet) of how it is setup. Below it I describe where I found to be the problem:
const Viewer = () => {
const [items, setItems] = useState(["inactive"]);
return (
<ItemSelect setItems={setItems} selected={items}/>
<DisplayItems items={items}/>
)
}
const ItemSelect = ({setItems, selected}) => {
const handleActiveItems = (activeItems) => {
setItems(activeItems);
}
return (
<SelectItems
handleActiveItems={handleActiveItems}
items={selected}
/>
)
}
const SelectItems = ({handleActiveItems, items}) => {
const [selected, setSelected] = useState([])
useEffect(() => {
setSelected(items);
}, [items]);
const randomTestItem = ["apple", "peach", "orange"];
const handleOnClick = (isSelected, item) => {
let tmpItems = items;
if (isSelected) {
let index = tmpItems.indexOf("inactive");
if (index > -1) {
handleActiveItems([option]);
} else {
handleActiveItems([...selected, option]);
}
} else if (!isSelected) {
let index = tmpItems.indexOf(option);
if (index > -1) {
tmpItems.splice(index, 1);
if (tmpItems.length === 0) {
handleActiveItems(["inactive"]);
} else {
handleActiveItems([tmpItems]);
}
}
}
}
return (
{
randomTestItem?.map((item,index) => {
return (
<DisplayClickable item={item} onClick={handleOnClick} key={index}/>
)
})
}
)
}
<DisplayClickable item={item} onClick={handleOnClick}/> holds a useState() that toggle from active/inactive.
I've tested this in many different area's I believe the crux of the problem to be here:
} else if (!isSelected) {
let index = tmpItems.indexOf(option);
if (index > -1) {
tmpItems.splice(index, 1);
if (tmpItems.length === 0) {
handleActiveItems(["inactive"]);
} else {
handleActiveItems([tmpItems]);
}
}
}
specifically:
} else {
handleActiveItems([tmpItems]);
}
When I unselect all the items and switch the array back to "inactive", everything updates instantly and exactly how you would expect. Selecting items always adds to the list correctly, it's removing them that everything goes wonky. I've done a console.log right before calling handleActiveItems() and the tmpItems array is always correct to what it should be. It just never updates the set state.
Within handleActiveItems the log also shows it is receiving the array just before setting it. It just never sets it.
I believe since you are using the splice method, you just modify the existing array and React does not recognize it as "updatable". You can try to use the filter method:
if (index > -1) {
const newArray = tmpItems.filter((_, itemIndex)=> itemIndex !== index)
if (newArray.length === 0) {
handleActiveItems(["inactive"]);
} else {
handleActiveItems(newArray);
}
}
With the code above, filter method will generate a new array.
Give it a try, hopefully it will help =)
update
I've just realized, maybe you don't need the extra [] you are putting into handleActiveItems(). So instead of:
handleActiveItems([tmpItems])
It could be just:
handleActiveItems(tmpItems)
I figured it out.
It all came down to this line:
let tmpItems = items;
Changing to this:
let tmpItems = [...items];
for some reason allowed React to pay more attention and notice that there was in fact a change.
I just changed in my development build and it works without a hiccup.

updating an array in React

I have an array of 16 objects which I declare as a state in the constructor:
this.state = {
todos:[...Array(16)].map((_, idx) => {
return {active: false, idx}
}),
}
Their status will get updated through an ajax call in ComponentDidMount.
componentDidMount()
{
var newTodos = this.state.todos;
axios.get('my/url.html')
.then(function(res)
{
newTodos.map((t)=>{
if (something something)
{
t.active = true;
}
else
{
t.active = false;
}
}
this.setState({
todos:newTodos,
})
}
}
and then finally, I render it:
render(){
let todos = this.state.todos.map(t =>{
if(t.active === true){
console.log('true'}
else{
console.log('false')
}
})
return (
<div></div>
)
}
They all appear as active = false in the console, they never go into the if condition. When
I print out the entire state it appears not to be updated in the render method. In the console it says "value below was just updated now".
I thought changes to the state in ComponentWillMount will call the render function again?
How do I make that React will accept the new values of the state?
componentDidMount()
{
var newTodos = []; // <<<<<<<<<<<<<
axios.get('my/url.html')
.then(function(res)
{
newTodos = this.state.todos.map((t)=>{ //<<<<<<<<<<<<<<<
if (something something)
{
t.active = true;
}
else
{
t.active = false;
}
return t; //<<<<<<<<<<<<<<<<<<
} // <<<<< are you missing a semi-colon?
this.setState({
todos:newTodos,
})
}
}
The map() argument (in your code) is a function, not an expression, so an explicit return must be provided. I.E.:
xxx.map( t => ( "return t is implicit" ) );
xxx.map( t => { "return t must be explicit" } );
And, as #DanielKhoroshko points out, your new variable points to this.state. And of course never, never, ever alter this.state directly. Since map() returns a new array, not the original as altered, that's why we use map() and not forEach()
That is because you are actually not providing any new state, but mutating it instead.
React uses shallow comparison be default (where to objects are equal if they reference the same memory address). And that's exactly what's happening here:
var newTodos = this.state.todos; // newTodos === this.state.todos
this.setState({ todos:newTodos }) // replaces two equal addresses, so it simply doesn't change anything
The easiest solution, though probably not the most performant would be to clone your todos array:
var newTodos = [...this.state.todos]; // newTodos !== this.state.todos

Categories