2

I'm quite new to Hooks and I am trying to build a small address book.

So I have two components:

  • A ContactCard component
  • A ContactsList component

I want cards to be removed when the X is clicked. I managed to toggle the deleted prop of my contact, but I can't figure out how to force re-render the ContactsList then

import React, { useState } from 'react'
import ContactsList from './components/contacts-list/contacts-list.component'
import './App.scss'

function App() {
  const [contacts] = useState([
    {
      key: 0,
      name: 'Lennon',
      firstname: 'John',
      notes: 'smart guy',
      deleted: false
    },
    {
      key: 1,
      name: 'Starr',
      firstname: 'Ringo',
      notes: 'funny guy',
      deleted: false
    }
  ])
  return (
    <div className='App'>
      <ContactsList contacts={contacts} />
    </div>
  )
}

export default App

import React, { useState, useEffect } from 'react'
import ContactCard from '../contact-card/contact-card.component'

import './contacts-list.styles.scss'

function ContactsList(props) {
  const [contacts, setContacts] = useState(props.contacts)

  return (
    <div className='contacts-list'>
      <span className='title'>Contacts</span>
      {contacts
        .filter(contact => contact.deleted === false)
        .map(contact => (
          <ContactCard
            name={contact.name}
            firstname={contact.firstname}
            notes={contact.notes}
            deleted={contact.deleted}
          />
        ))}
      <hr />
    </div>
  )
}

export default ContactsList

import React, { useState } from 'react'

import './contact-card.styles.scss'

function ContactCard(props) {
  const [contact, setContact] = useState([
    {
      name: props.name,
      firstname: props.firstname,
      notes: props.notes,
      deleted: false
    }
  ])

  function deleteContact() {
    const currentContact = [...contact]
    currentContact[0].deleted = true
    setContact(currentContact)
  }

  return (
    <div className='contact-card'>
      <span className='contact-name'>{props.name}</span>
      <span className='delete-contact' onClick={deleteContact}>
        &#10005;
      </span>
      <br />
      <span className='contact-firstname'>{props.firstname}</span>
      <hr className='separator' />
      <span className='contact-notes'>{props.notes}</span>
    </div>
  )
}

export default ContactCard
Brendan McGill
  • 6,130
  • 4
  • 21
  • 31
sdrai
  • 23
  • 3

2 Answers2

1

Really a few options here, the simplest is probably just to pass in an 'onContactDeleted' prop and callback to the parents to let them know to update the state. This method isn't always the cleanest, especially with highly nested components but I would recommend it as a start is as it really is the most vanilla way that will help you understand how prop and state changes work. Note that I kept your soft delete method but you could also just remove it from the list.

Card

import React, { useState } from 'react'

import './contact-card.styles.scss'

function ContactCard(props) {

  function deleteContact(key) {
    props.onContactDeleted(key)
  }

  return (
    <div className='contact-card'>
      <span className='contact-name'>{props.name}</span>
      <span className='delete-contact' onClick={() => deleteContact(props.contactKey)}>
        &#10005;
      </span>
      <br />
      <span className='contact-firstname'>{props.firstname}</span>
      <hr className='separator' />
      <span className='contact-notes'>{props.notes}</span>
    </div>
  )
}

export default ContactCard

List

import React, { useState, useEffect } from 'react'
import ContactCard from '../contact-card/contact-card.component'

import './contacts-list.styles.scss'

function ContactsList(props) {
  const [contacts, setContacts] = useState(props.contacts)

  return (
    <div className='contacts-list'>
      <span className='title'>Contacts</span>
      {contacts
        .filter(contact => contact.deleted === false)
        .map(contact => (
          <ContactCard
            contactKey={contact.key}
            name={contact.name}
            firstname={contact.firstname}
            notes={contact.notes}
            deleted={contact.deleted}
            onContactDeleted={props.onContactDeleted}
          />
        ))}
      <hr />
    </div>
  )
}

export default ContactsList

App

import ContactsList from './components/contacts-list/contacts-list.component'
import './App.scss'

function App() {
  const [contacts, setContacts] = useState([
    {
      key: 0,
      name: 'Lennon',
      firstname: 'John',
      notes: 'smart guy',
      deleted: false
    },
    {
      key: 1,
      name: 'Starr',
      firstname: 'Ringo',
      notes: 'funny guy',
      deleted: false
    }
  ])
  return (
    <div className='App'>
      <ContactsList contacts={contacts} 
                    onContactDeleted={
                      (key_to_delete) => {
                         //note this might not be correct, use it as pseudocode
                         var copy = [...contacts]
                         var contact = copy.find(x => x.key == key_to_delete)
                         if(contact)
                         {
                            contact.deleted = true;
                            setContacts(copy)
                         }
                      }
                     }/>
    </div>
  )
}

export default App

Once you have that you could use something like redux or the useContext hook to share the state and "cut out the middle man"

Here is an example of the useContext hook that I quickly found online, not sure how good it is

https://www.codementor.io/@sambhavgore/an-example-use-context-and-hooks-to-share-state-between-different-components-sgop6lnrd

Zach
  • 1,964
  • 2
  • 17
  • 28
  • Hi ! Thanks a lot for your help. To be honest, I used a softdelete because I couldn't figure out how to hard delete the item from the list. Do you think I just thould slice the element from the array whenever the click is triggered ? Thanks again for your help :) – sdrai Jan 08 '20 at 21:01
  • You bet! You could slice but I usually use filter as it returns a new array and you shouldn't modify state variables directly anyway so it saves time on using the spread operator like the soft delete example above. So something like this: `setContacts(contacts.filter(x => x.key != key_to_delete))`. This function just returns a new array of everything that isn't the key you deleted – Zach Jan 09 '20 at 17:59
0

Starting from your App, I would suggest you to move your contacts object to a constant, as useState is not needed at this level.

// constants.js
export const contacts = [
    {
      key: 0,
      name: 'Lennon',
      firstname: 'John',
      notes: 'smart guy'
    },
    {
      key: 1,
      name: 'Starr',
      firstname: 'Ringo',
      notes: 'funny guy'
    }
  ]
};

// App.js
import React from 'react';
import { contacts } from './constants';
import ContactsList from './components/contacts-list/contacts-list.component'
import './App.scss'

function App() {
  return (
    <div className='App'>
      <ContactsList contacts={contacts} />
    </div>
  )
}

export default App

Moving on ContactList component, as it's the component that renders each contact, I would build my state here. In that why, I would know if I need to render a contact or not beforehand.

import React, { useState } from 'react'
import ContactCard from '../contact-card/contact-card.component'

import './contacts-list.styles.scss'

function ContactsList(props) {
  const [contacts, setContacts] = useState(props.contacts);
  const handleDeletion = id => {
    setContacts(contacts.filter(contact => contact.id !== id));
  }

  return (
    <div className='contacts-list'>
      <span className='title'>Contacts</span>
      {contacts.length ? 
        contacts.map(contact => 
          <ContactCard
            id={contact.id}
            name={contact.name}
            firstname={contact.firstname}
            notes={contact.notes}
            handleDeletion={handleDeletion}
          />
        ) : null}
      <hr />
    </div>
  )
}

export default ContactsList

Notice that I am passing the function that handles the removal to my ContactCard, while I am still deciding here if I should show my contact.

import React from 'react'

import './contact-card.styles.scss'

function ContactCard(props) {
  return (
    <div className='contact-card'>
      <span className='contact-name'>{props.name}</span>
      <span className='delete-contact' onClick={() => props.handleDeletion(props.id)}>
        &#10005;
      </span>
      <br />
      <span className='contact-firstname'>{props.firstname}</span>
      <hr className='separator' />
      <span className='contact-notes'>{props.notes}</span>
    </div>
  )
}

export default ContactCard

I haven't tried the code, but I think you should move at this path.

Michalis Garganourakis
  • 2,872
  • 1
  • 11
  • 24
  • Hi ! Thanks a lot for your help, I see now how to handle the deletion. As I asked below on the second answer, do you think that a hard delete would be better ? Thanks again for your help, appreciate it :) – sdrai Jan 08 '20 at 21:03
  • Happy to help! :) What do you mean by hard delete? Removing the object of the deleted contact from the array? – Michalis Garganourakis Jan 08 '20 at 21:07
  • It's mostly based on your codebase and what you are trying to achieve. If your state here, serves just the purpose I see above (where you don't use the deleted contacts anywhere on this two component sharing the state), you don't need `deleted` property on your object. Just render all your contacts from state, and remove the contact you don't want (also from your state, as demostrated above). Remember that you can always retrieve all your contacts from your `constants.js` if you need them somewhere else. I will update my answer above based on this. – Michalis Garganourakis Jan 08 '20 at 21:28
  • 1
    My goal is to build a small CRUD with React Hooks. But since I didn't do any back-end yet, I'm using placeholder constants. Anyway, thanks a lot again dude :) – sdrai Jan 08 '20 at 21:29
  • 1
    Good luck with your app! :) If this solved your problem, consider marking the answer as accepted by clicking on the check mark beside the answer to toggle it from greyed out to filled in. In that way, you help others with similar issues find their way :) – Michalis Garganourakis Jan 08 '20 at 21:33