2
\$\begingroup\$

In my project, I am getting an array of objects from api, and I want to divide the array into 3 three arrays based on the value of the shelf property of each array element which is an object. Now for doing that I am using formatBooks function inside the same component.

Later on I am passing this formatBooks function as prop to other component which calls this function.

I was hoping if there is any better by making another component and place the formatBooks inside that. I wanted to know a better and elegant way of doing things like this.

class BooksApp extends React.Component {
  state = {
    books: [],
    searchResults: [],
    newBooksFromSearch: [],
  }
  //format the books array
  formatBooks = () => {
    const currentlyReading = []
    const read = []
    const wantToRead = []
    this.state.books.forEach( item => {
      if(item.shelf === "currentlyReading" || item.shelf === "Currently Reading"){
          item.setShelf = "Currently Reading"
          currentlyReading.push(item)
      } 
      if(item.shelf === "read" || item.shelf === "Read" ){
          item.setShelf = "Read"
          read.push(item)
      }    
      if(item.shelf === "wantToRead" || item.shelf === "Want to Read"){
          item.setShelf = "Want to Read"
          wantToRead.push(item) 
      } 
    })
    return(
      {
        currentlyReading: currentlyReading,
        read: read,
        wantToRead: wantToRead
      }
    )
  }
\$\endgroup\$
1
\$\begingroup\$

You may start to make your code slightly less verbose (and with all the required semicolons):

const result = {
    currentlyReading: [],
    read: [],
    wantToRead: []
};

this.state.books.forEach(item => {
    if (...) {
        item.setShelf = "Currently Reading";
        result.currentlyReading.push(item);
    }
});

return result;


Then you might simplify comparison using regular expressions:

if (item.shelf.match(/^currently\s?reading$/i)) { ... }


Now you can see that you are MUTATING THE STATE without using setState(). That's an Incredibly Bad Thing™. You do not give enough context to provide a specific suggestion then I'll mention few:

1) Do not mutate existing objects, create new ones:

if (item.shelf.match(/^currently\s?reading$/i) {
    result.currentlyReading.push({ ...item, setShelf: "Currently Reading" });
}

2) Perform this translation in componentDidUpdate() when you populate the books state property.

3) If you're using Redux then perform this translation in your Reducer (or Selector, if you're using them).


Re-think your design. Why do you need to duplicate an information you already have? What do you need these lists (with slightly changed values) for if they're already there in state.books? Let's imagine an hypothetical render() method:

render() {
    const { books } = this.state;

    return (
        <React.Fragment>
            <Shelf title="Currently reading" books={books} filter="^currently\\s?reading$" />
            <Shelf title="Read" books={books} filter="^read$" />
            <Shelf title="Want to read" books={books} filter="^want\\s?to\\s?read$" />
        </React.Fragment>
    );
}

Your separate lists are now obsolete. If you do not care about localization then regex for filter attribute might even be calculated directly (inside Shelf component!) with something like:

const filter = new RegExp(`^${this.props.title.replace(" ", "\\s?")}$`, "i");

Even if I am not doing this in my example please note that regular expression text MUST be escaped (before calling .replace()): Escape string for use in Javascript regex.


Now that you have a separate component...you may consider to move selection again outside and use memoization, simplified example (pseudo-code for memoization, pick the package you prefer):

render() {
    const { books } = this.state;

    const read = memoize((list, filter) => list.filter(book => book.shelf.match(filter)));

    return (
        <React.Fragment>
            <Shelf title="Currently reading" books={read(books, /^read$/i)} />
        </React.Fragment>
    );
}

Note that I'm NOT using the setShelf property any more in favor of Shelf.title property.

\$\endgroup\$
1
\$\begingroup\$

Yes, there is a more elegant way.
I recommend creating a new utility module to hold your formatBooks function. Import the module into any component that needs it.

A good approach is:

  1. Create very simple Controls.
  2. Create the data you wish you had to keep your control simple.
  3. Test your control, with that data.
  4. Select/transform your data from the data you have to the data you want.
  5. Finally use your transformed data

1. Create very simple Controls.

const Book = props => (<li>{props.title}</li>)
const Header = props => (<li className='header'>{props.shelfTitle}</li>)
const Group = props => {
    const {shelfTitle, books} = props;
    const booksMap = title => (<Book title={title}/>)
    const booksList = books.map(booksMap)
    //prefer <React.Fragment> over <div>
    return (
        <div>
            <Header shelfTitle={shelfTitle}/>
            {booksList}
        </div>)
}
const GroupList = props => {
    const groupMap = d => (<Group  shelfTitle={d.shelfTitle} books={d.books}/>)
    const booksList = props.bookData.map(groupMap)
    return (<ul>
        {booksList}
    </ul>)
}

2. Create the data you wish you had to keep your control simple.

const groupedBooksData = [
    {shelfTitle:"Read", books: ["Deep Work"]},
    {shelfTitle: "Want To Read", books:["Antifragile", "MindSet"]}
]

3. Test your control, with that data.

const groupedBooksData = [
    {shelfTitle:"Read", books: ["Deep Work"]},
    {shelfTitle: "Want To Read", books:["Antifragile", "MindSet"]}
]
const Book = props => (<li>{props.title}</li>)
const Header = props => (<li className='header'>{props.shelfTitle}</li>)
const Group = props => {
    const {shelfTitle, books} = props;
    const booksMap = title => (<Book title={title}/>)
    const booksList = books.map(booksMap)
    //prefer <React.Fragment> 
    return (
        <div>
            <Header shelfTitle={shelfTitle}/>
            {booksList}
        </div>)
}
const GroupList = props => {
    const groupMap = d => (<Group  shelfTitle={d.shelfTitle} books={d.books}/>)
    const booksList = props.bookData.map(groupMap)
    return (<ul>
        {booksList}
    </ul>)
}

// Render it
ReactDOM.render(
<GroupList bookData={groupedBooksData} />,
document.getElementById("react")
);
li {
  list-style-type: none;
}

.header {
  font-weight: bold;
  width: 100%;
  color: white;
  background-color: black;
}

li {
  padding: 4px;
}
<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

4. Select/transform your data from the data you have to the data you want. Often data you get from an api isn't in the form/shape you want. This is a good time to practice TDD. Avoid forEach in favor of map, reduce, filter and sort.

//data you have
const books = [  
    { title: "Deep Work", shelf: "read" },
    { title: "Antifragile", shelf: "wantToRead"  },
    { title: "Mindset", shelf: "wantToRead" },  
    { title: "How to Win Friends and Influence People", shelf: "currentlyReading"  },
    { title: "The Power of Habit", shelf: "read"  },    
    { title: "Fooled By Randomness", shelf: "listenTo" }
]

const reduceToLists = (accumulator, item, i) => { 
    const oldTable = accumulator[item.shelf] || []
    oldTable.push(item.title)
    accumulator[item.shelf] = oldTable
    console.log(i, item, accumulator)
    return accumulator; 
}
const formatBooks = list => list.reduce(reduceToLists, {})
const result = formatBooks(books)
// reduceToLists is called books.length times and 
// has a console.log(
//i              item                                accumulator)
//0 {title: "Deep Work", shelf: "read"}         {read: [Deep Work]}
//1 {title: "Antifragile", shelf: "wantToRead"} {read: [Deep Work], wantToRead: [Antifragile]}
//2 {title: "Mindset", shelf: "wantToRead"}     {read: [Deep Work], wantToRead: [Antifragile, Mindset]}
//.....

console.log("Data you want", result);

Prefer pure functions and immutable data. This function is equivalent to reduceToLists but is pure and immutable.

//Data you have
const books = [  
    { title: "Deep Work", shelf: "read" },
    { title: "Antifragile", shelf: "wantToRead"  },
    { title: "Mindset", shelf: "wantToRead" },  
    { title: "How to Win Friends and Influence People", shelf: "currentlyReading"  },
    { title: "The Power of Habit", shelf: "read"  },
    { title: "Clean Code", shelf: "read"  },
    { title: "Fooled By Randomness", shelf: "listenTo" },
    { title: "Purple Cow", shelf: "read"  },
    
]

const reduceToLists = (accumulator, item, i) => { 
    const oldTable = accumulator[item.shelf] || []
    oldTable.push(item.title)
    accumulator[item.shelf] = oldTable
    console.log(i, item, accumulator)
    return accumulator; 
}
const formatBooks = list => list.reduce(reduceToLists, {})


const titleCaseMap = {
    wantToRead: "Want To Read",
    read: "Read",
    currentlyReading: "Currently Reading",
    listenTo: "Listen To"
}// actually would use: https://github.com/blakeembrey/change-case 


//use formatBooks
const transformListOfBooks = books => {
    const booksObj = formatBooks(books); //{read: [Deep Work, ...], wantToRead: [Antifragile, Mindset, ...]}
    const keys = Object.keys(booksObj); //[read, wantToRead, currentlyReading, ...]
    const mapToTitle = (key, i) => { 
        const result = {
            title: titleCaseMap[key],
            list: booksObj[key]
        };
        console.log(i, key, result)
        return result;
    }
    //without console.log above can be simplified to 
    //const mapToTitle = key => ({title: titleCaseMap[key], list: booksObj[key]}) 
    return keys.map(mapToTitle)  
}

console.log("Data you want", transformListOfBooks(books))

5. Finally use selected data

//Data you have
const books = [  
    { title: "Deep Work", shelf: "read" },
    { title: "Antifragile", shelf: "wantToRead"  },
    { title: "Mindset", shelf: "wantToRead" },  
    { title: "How to Win Friends and Influence People", shelf: "currentlyReading"  },
    { title: "The Power of Habit", shelf: "read"  },
    { title: "Clean Code", shelf: "read"  },
    { title: "Fooled By Randomness", shelf: "listenTo" },
    { title: "Purple Cow", shelf: "read"  },
    
]

const reduceToLists = (accumulator, item, i) => { 
    const oldTable = accumulator[item.shelf] || []
    oldTable.push(item.title)
    accumulator[item.shelf] = oldTable
    console.log(i, item, accumulator)
    return accumulator; 
}
const formatBooks = list => list.reduce(reduceToLists, {})


const titleCaseMap = {
    wantToRead: "Want To Read",
    read: "Read",
    currentlyReading: "Currently Reading",
    listenTo: "Listen To"
}// actually would use: https://github.com/blakeembrey/change-case 


//use formatBooks
const transformListOfBooks = books => {
    const booksObj = formatBooks(books); //{read: [Deep Work, ...], wantToRead: [Antifragile, Mindset, ...]}
    const keys = Object.keys(booksObj); //[read, wantToRead, currentlyReading, ...]
    const mapToTitle = key => ({shelfTitle: titleCaseMap[key], books: booksObj[key]}) 
    return keys.map(mapToTitle)  
}



const Book = props => (<li>{props.title}</li>)
const Header = props => (<li className='header'>{props.shelfTitle}</li>)
const Group = props => {
    const {shelfTitle, books} = props;
    const booksMap = title => (<Book title={title}/>)
    const booksList = books.map(booksMap)
    //prefer <React.Fragment> 
    return (
        <div>
            <Header shelfTitle={shelfTitle}/>
            {booksList}
        </div>)
}
const GroupList = props => {
    const groupMap = d => (<Group  shelfTitle={d.shelfTitle} books={d.books}/>)
    const booksList = props.bookData.map(groupMap)
    return (<ul>
        {booksList}
    </ul>)
}
const dataYouWant= transformListOfBooks(books);
// Render it
ReactDOM.render(
<GroupList bookData={dataYouWant} />,
document.getElementById("react")
);
li {
  list-style-type: none;
}

.header {
  font-weight: bold;
  width: 100%;
  color: white;
  background-color: black;
}

li {
  padding: 4px;
}
<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.