при обновлении state react не перерисовывает приложение

Всем привет! Недавно начал изучать React и столкнулся со следующей проблемой. При отправке формы изменяю state (через переданную функцию updateMovies). State меняется (проверяю это через console.log в render() в компоненте App), но сама страница не перерисовывается. Хотя если что-то изменить в коде и сохранить, изменения отобразятся. Проект на GitHub

(Api с помощью которого получаю информацию о фильмах без впн не работает!!!)

App

import React, { Component } from 'react';
import MovieList from '../movie-list/movie-list';
import getListMovie from '../../server-api/get-list-movie';
import { Offline, Online } from 'react-detect-offline';
import './app.css';
import Search from '../search/search';
import { Pagination } from 'antd';

export default class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      movies: [],
    };
  }

  componentDidMount() {
    getListMovie()
      .then((movies) => {
        this.setState({ movies: movies.results });
      })
      .catch((err) => {
        console.error('Error:', err);
      });
  }

  // Метод для обновления списка фильмов
  updateMovies = (movies) => {
    this.setState({ movies });
  };

  addNewMovie = (movie) => {
    this.setState((prevState) => ({
      movies: [...prevState.movies, movie],
    }));
  };

  render() {
    console.log('Rendered movies:', this.state.movies);
    const { movies } = this.state;
    return (
      <div className="app">
        <Online>
          <Search updateMovies={this.updateMovies} />
          <MovieList movies={movies} addNewMovie={this.addNewMovie} />
        </Online>
        <Offline>
          <span>Sorry not connection</span>
        </Offline>
      </div>
    );
  }
}

Search

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import './search.css';
import filmSearch from '../../server-api/film-search';

export default class Search extends Component {
  constructor(props) {
    super(props);
    this.state = {
      label: '',
    };
  }

  onSubmit = (e) => {
    e.preventDefault();
    const { label } = this.state;
    const { updateMovies } = this.props; 

    if (label.trim()) {
      filmSearch(label)
        .then((searchInfo) => {
          updateMovies(searchInfo.results); 
        })
        .catch((error) => {
          console.error('Error searching for movies:', error);
        });

      this.setState({
        label: '',
      });
    }
  };

  onLabelChange = (e) => {
    const { value } = e.target;
    this.setState({
      label: value,
    });
  };

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

    return (
      <form onSubmit={this.onSubmit} className="search">
        <input
          className="search-input"
          placeholder="Type to search..."
          onChange={this.onLabelChange}
          value={label}
        />
      </form>
    );
  }
}

Search.propTypes = {
  updateMovies: PropTypes.func.isRequired,
};

MovieList

import React from 'react';
import Movie from '../movie/movie';
import PropTypes from 'prop-types';
import './movie-list.css';

function MovieList({ movies, addNewMovie }) {

  const elements = movies.map((item) => {
    const { id } = item;

    return <Movie id={id} addNewMovie={addNewMovie} />;
  });

  return <ul className="movie-list">{elements}</ul>;
}

MovieList.propTypes = {
  movies: PropTypes.arrayOf(
    PropTypes.shape({
      id: PropTypes.number.isRequired, 
      title: PropTypes.string.isRequired,
    })
  ).isRequired, 
  addNewMovie: PropTypes.func.isRequired, 
};

export default MovieList;

Movie

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import './movie.css';
import { Spin, Alert } from 'antd';
import getMovieInfo from '../../server-api/get-movie-info';

export default class Movie extends Component {
  state = {
    loading: true,
    hasError: false,
    errorMessage: '',
    movieInfo: null,
  };

  componentDidMount() {
    const { id, addNewMovie } = this.props;

    getMovieInfo(id)
      .then((movieInfo) => {
        addNewMovie(id, movieInfo); 
        this.setState({ movieInfo, loading: false });
      })
      .catch((error) => {
        this.setState({
          hasError: true,
          errorMessage: error.message || 'Failed to fetch movie information.',
          loading: false,
        });
      });
  }

  
  truncateText = (text, maxLength) => {
    if (!text) return '';
    if (text.length <= maxLength) return text;

    let truncated = text.slice(0, maxLength);
    if (text[maxLength] !== ' ' && truncated[truncated.length - 1] !== ' ') {
      truncated = truncated.slice(0, truncated.lastIndexOf(' '));
    }

    return `${truncated}...`;
  };

  render() {
    const { loading, hasError, errorMessage, movieInfo } = this.state;

    if (hasError) {
      return (
        <div className="movie-error">
          <Alert
            message="Error"
            description={errorMessage}
            type="error"
            showIcon
          />
        </div>
      );
    }

    if (loading) {
      return (
        <div className="movie-spinner">
          <Spin size="large" tip="Loading movie details..." />
        </div>
      );
    }

    if (!movieInfo) {
      return <p className="movie-not-found">Movie information not found.</p>;
    }

    return (
      <div className="movie">
        <MovieInfo movieInfo={movieInfo} truncateText={this.truncateText} />
      </div>
    );
  }
}

Movie.propTypes = {
  id: PropTypes.number.isRequired,
  movies: PropTypes.object.isRequired, 
  addNewMovie: PropTypes.func.isRequired,
};

const MovieInfo = ({ movieInfo, truncateText }) => {
  const { title, release_date, poster_path, overview, genres } = movieInfo;

  return (
    <React.Fragment>
      <img
        className="movie-image"
        src={`https://image.tmdb.org/t/p/w500${poster_path}`}
        alt={`${title} poster`}
      />
      <div className="container">
        <h5 className="movie-title">{title}</h5>
        <span className="movie-date">{release_date}</span>
        
        {genres && genres.length > 0 && (
          <div className="movie-categories">
            {genres.map((genre) => (
              <div key={genre.id} className="movie-genre">
                {genre.name}
              </div>
            ))}
          </div>
        )}
        <div className="movie-description">
          {truncateText(overview || 'No description available', 200)}
        </div>
      </div>
    </React.Fragment>
  );
};

MovieInfo.propTypes = {
  movieInfo: PropTypes.shape({
    title: PropTypes.string.isRequired,
    release_date: PropTypes.string.isRequired,
    poster_path: PropTypes.string,
    description: PropTypes.string.isRequired,
    genres: PropTypes.arrayOf(
      PropTypes.shape({
        id: PropTypes.number.isRequired,
        name: PropTypes.string.isRequired,
      })
    ),
  }).isRequired,
  truncateText: PropTypes.func.isRequired,
};

Ответы (2 шт):

Автор решения: ksa

При отправке формы изменяю state (через переданную функцию updateMovies). State меняется (проверяю это через console.log в render() в компоненте App), но сама страница не перерисовывается.

Есть мнение... (с) Что происходит потеря контекста.

Вот такой способ передачи метода <Search updateMovies={this.updateMovies} /> возможен, если метод "забиндить" на нужный контекст

export default class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      movies: [],
    };
    this.updateMovies = this.updateMovies.bind(this)
  }
}

Или вызывать метод в правильном контексте

<Search updateMovies={m => this.updateMovies(m)} />
→ Ссылка
Автор решения: Никита Ельяшевич

При формировании списка в map обязательно указывать key, иначе render работает не корректно

function MovieList({ movies, addNewMovie }) {
  return (
    <ul className="movie-list">
      {movies.map((item) => {
        return (
          <Movie
            id={item.id}
            key={item?.id?.toString()}
            addNewMovie={addNewMovie}
          />
        );
      })}
    </ul>
  );
}
→ Ссылка