backslash
backslash

Reputation: 304

React Stateless Components: Interacting with their output and appearance

I have looked around for an answer to this - the closest I found being this question - but there is I think a significant difference in my case (the fact that it starts to get into the parent holding the state of its children's... children) which has finally lead to me asking for some clarification.

A very simple example of what I mean is below (and will hopefully better illustrate what I'm asking):

Suppose we have a bunch of book documents like

bookList = [
{
  title: "book 1", 
  author: "bob", 
  isbn: 1, 
  chapters: [
{ chapterNum: 1, chapterTitle: "intro", chapterDesc: "very first chapter", startPg: 2, endPg: 23 }, 
{ chapterNum: 2, chapterTitle: "getting started", chapterDesc: "the basics", startPg: 24, endPg: 45 }
]},
{
 title: "book 2" ... }
]

So main point being these embedded objects within documents that could be very long and as such may be collapsed / expanded.

And then here is a rough sample of code showing the components

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            books: bookList,
            focusBook: null
        }
        this.updateDetailDiv = this.updateDetailDiv.bind(this);
    }

    updateDetailDiv(book) {
        this.setState(
            { focusBook: book}
        );
    }

    render() {
        return(
            <BookList 
                bookList = {this.state.books} 
                updateDetailDiv = { this.updateDetailDiv }
            />
            <BookDetail
                focusBook = { this.state.focusBook }
            />
        );
    }
}

const BookList = props => {
    return (
        props.bookList.map(item=>
            <li onClick={()=> props.updateDetailDiv(item)}> {item.title} </li>
            )
        );
}

const BookDetail = props => {
     return (
        <div className="bookDetails">
        { props.focusBook != null
        ? <div>
            {props.focusBook.title}, 
            {props.focusBook.author},
            {props.focusBook.isbn}
            Chapters:
            <div className="chapterList">
                { props.focusBook.chapters.map(item=>
                    <span onClick={()=>someFunction(item)}>{item.chapterNum} - {item.chapterName}</span>
                    )}
            </div>
            <div id="chapterDetails">
                This text will be replaced with the last clicked chapter's expanded details
            </div>
        </div>
        : <div>
            Select A Book
         </div> 
     })
}

someFunction(item) {
 document.getElementById('chapterDetails').innerHTML = `<p>${item.chapterDesc}</p><p>${item.startPg}</p><p>${item.endPg}</p>`;
}

So my problem is that i'm not sure what the best approach is for handling simple cosmetic / visual changes to data in functional stateless components without passing it up to the parent component - which is fine and makes sense for the first child - but what happens when many children will have their own children (who may have their own children) --> all requiring their own rendering options? For example - here the App component will re-render the DetailDiv component (since the state has changed) - but I don't want the App also handling the DetailDiv's detailed div. In my example here its all very simple but the actual application I'm working on has 2 or 3 layers of embedded items that - once rendered by App - could realisticially just be modified visually by normal JS.

SO in my example you'll see I have a someFunction() in each Chapter listing - I can make this work by writing a separate simple 'traditional JS DOM function' (ie: target.getElementById or closest() -- but i don't think i'm supposed to be using normal JS to manipulate the DOM while using React.

So again to summarize - what is the best way to handle simple DOM manipulation to the rendered output of stateless components? Making these into their own class seems like overkill - and having the 'parent' App handle its 'grandchildren' and 'great-grandchildren's state is going to be unwieldy as the Application grows. I must be missing an obvious example out there because I haven't seen much in the way of handling this without layers of stateful components.

EDIT for clarity:

BookDetail is a stateless component.

It is handed an object as a prop by a parent stateful component (App)

When App's state is changed, it will render again, reflecting the changes.

Assume BookDetail is responsible for displaying a lot of data.

I want it so each of the span in BookDetail, when clicked, will display its relevant item in the chapterDetail div. If another span is clicked, then the chapterDetail div would fill with that item's details. (this is just a simple example - it can be any other pure appearance change to some stateless component - where it seems like overkill for a parent to have to keep track of it)

I don't know how to change the UI/appearance of the stateless component after it is rendered without giving it state OR making the parent keep track of what is essentially a 'substate' (since the only way to update the appearance of a component is to change its state, triggering a render).

Is there a way to do this without making BookDetail a stateful component?

Upvotes: 1

Views: 148

Answers (2)

Drew Reese
Drew Reese

Reputation: 203417

You can add a little bit of simple state to functional components to track the selected index. In this case I would store a "selected chapter index" in state and then render in the div the "chapters[index].details", all without manipulating the DOM which is a React anti-pattern.

The use-case here is that the selected chapter is an internal detail that only BookDetail cares about, so don't lift this "state" to a parent component and since it is also only relevant during the lifetime of BookDetail it is rather unnecessary to store this selected index in an app-wide state management system, like redux.

const BookDetail = ({ focusBook }) => {
  // use a state hook to store a selected chapter index
  const [selectedChapter, setSelectedChapter] = useState();

  useEffect(() => setSelectedChapter(-1), [focusBook]);

  if (!focusBook) {
    return <div>Select A Book</div>;
  }

  const { author, chapters, isbn, title } = focusBook;

  return (
    <div className="bookDetails">
      <div>
        <div>Title: {title},</div>
        <div>Author: {author},</div>
        <div>ISBN: {isbn}</div>
        Chapters:
        <div className="chapterList">
          {chapters.map(({chapterName, chapterNum}, index) => (
            <button
              key={chapterName}
              onClick={() => setSelectedChapter(selectedChapter >= 0 ? -1 : index)} // set the selected index
            >
              {chapterNum} - {chapterName}
            </button>
          ))}
        </div>

        // if a valid index is selected then render details div with 
        // chapter details by index
        {chapters[selectedChapter] && (
          <div id="chapterDetails">
            {chapters[selectedChapter].details}
          </div>
        )}
      </div>
    </div>
  );
};

DEMO

Edit adoring-chandrasekhar-yt3y1

Upvotes: 1

Celso Wellington
Celso Wellington

Reputation: 889

There is some approaches you can do to solve this problem. First, you don't need to create some class components for your functional components, instead, you can use react hooks, like useState so the component can control it's own content.

Now, if you don't want to use React Hooks, you can use React Redux store to manage all your states: you can only change the state values using the Redux actions.

Happy coding! :D

Upvotes: 1

Related Questions